Kutluhan Aktar
Published © CC BY

AI-assisted Air Quality Monitor w/ IoT Surveillance

Log NO2, O3, and weather data, train a NN model to detect air pollution & display real-time results w/ surveillance footage on a PHP web app

ExpertFull instructions provided3,788

Things used in this project

Hardware components

FireBeetle ESP32 IOT Microcontroller (Supports Wi-Fi & Bluetooth)
DFRobot FireBeetle ESP32 IOT Microcontroller (Supports Wi-Fi & Bluetooth)
×1
DFRobot FireBeetle Covers - Camera&Audio Media Board
×1
Arduino Mega 2560
Arduino Mega 2560
×1
LattePanda 3 Delta 864
×1
DFRobot Gravity: Electrochemical Nitrogen Dioxide Sensor
×1
DFRobot Gravity: Electrochemical Ozone Sensor
×1
Anemometer Kit (0-5V)
DFRobot Anemometer Kit (0-5V)
×1
DHT22 Temperature and Humidity Sensor
×1
SH1106 OLED Display (128x64)
×1
Creality Sermoon V1 3D Printer
×1
Creality Sonic Pad
×1
Creality CR-200B 3D Printer
×1
Keyes 10mm RGB LED Module (140C05)
×1
SparkFun Logic Level Converter - Bi-Directional
SparkFun Logic Level Converter - Bi-Directional
×1
Adafruit Button (6x6)
×3
Solderless Breadboard Half Size
Solderless Breadboard Half Size
×2
SparkFun Solder-able Breadboard - Mini
SparkFun Solder-able Breadboard - Mini
×1
DFRobot 8.9' 1920x1200 IPS Touch Display (Optional)
×1
Xiaomi 20000 mAh 3 Pro Type-C Power Bank
×1
USB Buck-Boost Converter Board
×1
Jumper wires (generic)
Jumper wires (generic)
×1

Software apps and online services

Edge Impulse Studio
Edge Impulse Studio
Arduino IDE
Arduino IDE
Thonny
Fusion 360
Autodesk Fusion 360
Ultimaker Cura
XAMPP
Visual Studio 2017
Microsoft Visual Studio 2017

Hand tools and fabrication machines

Hot glue gun (generic)
Hot glue gun (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

Air_Quality_Monitor_main_case.stl

Air_Quality_Monitor_sliding_cover.stl

Air_Quality_Monitor_camera_holder.stl

Edge Impulse Model (Arduino Library)

Schematics

FireBeetle ESP32

FireBeetle Media Board

Code

AIoT_weather_station_run_model.ino

Arduino
         /////////////////////////////////////////////  
        //     AI-assisted Air Quality Monitor     //
       //          w/ IoT Surveillance            //
      //             ---------------             //
     //            (FireBeetle ESP32)           //           
    //             by Kutluhan Aktar           // 
   //                                         //
  /////////////////////////////////////////////

//
// Log NO2, O3, and weather data, train a NN model to detect air pollution, and display real-time results w/ surveillance footage on a PHP web app.
//
// For more information:
// https://www.theamplituhedron.com/projects/AI_assisted_Air_Quality_Monitor_w_IoT_Surveillance
//
//
// Connections
// FireBeetle ESP32 :  
//                                Arduino Mega
// D4   --------------------------- D18 (RX1)
// D2   --------------------------- D19 (TX1)


// Include the required libraries:
#include <Arduino.h>
#include <WiFi.h>
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "esp_camera.h"
#include "FS.h"
#include "SD_MMC.h"

// Include the Edge Impulse model converted to an Arduino library:
#include <AI-assisted_Air_Quality_Monitor_inferencing.h>

// Define the required parameters to run an inference with the Edge Impulse model.
#define FREQUENCY_HZ        EI_CLASSIFIER_FREQUENCY
#define INTERVAL_MS         (1000 / (FREQUENCY_HZ + 1))

// Define the features array to classify one frame of data.
float features[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE];
size_t feature_ix = 0;

// Define the threshold value for the model outputs (predictions).
float threshold = 0.60;

// Define the air quality level (class) names:
String classes[] = {"Clean", "Risky", "Unhealthy"};

char ssid[] = "<_SSID_>";        // your network SSID (name)
char pass[] = "<_PASSWORD_>";    // your network password (use for WPA, or use as key for WEP)
int keyIndex = 0;                // your network key Index number (needed only for WEP)

// Define the server on LattePanda 3 Delta 864.
char server[] = "192.168.1.22";
// Define the web application path.
String application = "/weather_station_data_center/update_data.php";

// Initialize the WiFiClient object.
WiFiClient client; /* WiFiSSLClient client; */

// FireBeetle Covers - Camera & Audio Media Board
// https://wiki.dfrobot.com/FireBeetle_Covers-Camera%26Audio_Media_Board_SKU_DFR0498
// Pinout:
#define PWDN_GPIO_NUM     -1
#define RESET_GPIO_NUM    0
#define XCLK_GPIO_NUM     21
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27
#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       19
#define Y4_GPIO_NUM       18
#define Y3_GPIO_NUM       5
#define Y2_GPIO_NUM       17
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22

// Define the camera (image) buffer array.
camera_fb_t * fb = NULL;

// Define the built-in button on the media board.
#define button  16

// Create a struct (data) including all air quality data parameters:
struct data {
  float temperature;
  float humidity;
  float no2;
  int ozone;
  int wind_speed;
};

// Define the data holders:
struct data air_quality;
int predicted_class = -1;
#define RXD  4
#define TXD  2
String data_packet = "";
String _header = "no2,ozone,temperature,humidity,wind_speed\n";
int del_1, del_2, del_3, del_4, del_5;
int c_s = 0, r_s = 0, u_s = 0;    
unsigned long model_timer = 0;

void setup(){
  // Disable the brownout detector.
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
  
  Serial.begin(115200);

  pinMode(button, INPUT_PULLUP);

  // Initialize the hardware serial port (2) to communicate with Arduino Mega.
  Serial2.begin(115200, SERIAL_8N1, RXD, TXD); // (BaudRate, SerialMode, RX_pin, TX_pin)

  // Initiate the built-in SD card module on the media board.
  if(!SD_MMC.begin()){
    Serial.println("SD Card not detected!\n");
    return;
  }
  Serial.println("SD Card detected successfully!\n");

  // Define the OV7725 camera pin configuration settings.
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  
  // Define the pixel format and the frame size settings.
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_GRAYSCALE;
  config.frame_size = FRAMESIZE_QVGA; // FRAMESIZE_96X96, FRAMESIZE_240X240
  config.jpeg_quality = 20; // 0-63 lower number means higher quality
  config.fb_count = 1;

  // No PSRAM
  config.fb_location = CAMERA_FB_IN_DRAM;

  // Initiate the OV7725 camera on the media board.
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    delay(1000);
    ESP.restart();
  }
  Serial.println("Camera initialized successfully!\n");
                   
  // Connect to WPA/WPA2 network. Change this line if using open or WEP network:
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, pass);
  // Attempt to connect to the Wi-Fi network:
  while(WiFi.status() != WL_CONNECTED){
    // Wait for the connection:
    delay(500);
    Serial.print(".");
  }
  // If connected to the network successfully:
  Serial.println("Connected to the Wi-Fi network successfully!");

  // Update the model timer.
  model_timer = millis();
}

void loop(){
  // Obtain the data packet and commands transferred by Arduino Mega via serial communication.
  if(Serial2.available() > 0){
    data_packet = Serial2.readString();
  }

  if(data_packet != ""){
    if(data_packet.startsWith("Save")){
      // Glean information as substrings from the transferred data packet by Arduino Mega.
      del_1 = data_packet.indexOf("&");
      del_2 = data_packet.indexOf("&", del_1 + 1);
      String data_record = data_packet.substring(del_1 + 1, del_2);
      String level = data_packet.substring(del_2 + 1);
      // Increment the sample number of the given level (class) by 1.
      int i;
      if(level == "Clean") { c_s+=1; i=c_s;}
      if(level == "Risky") { r_s+=1; i=r_s;}
      if(level == "Unhealthy") { u_s+=1; i=u_s;}
      // Save the transferred data record as a sample (CSV file) depending on the given air quality level.
      String file_name = "/samples/" + level + ".training.sample_" + String(i) + ".csv";
      String line = _header + data_record;
      save_data_to_CSV(file_name.c_str(), line.c_str(), file_name);
    }
    if(data_packet.startsWith("Data")){
      // Glean information as substrings from the transferred data packet by Arduino Mega.
      del_1 = data_packet.indexOf(",");
      del_2 = data_packet.indexOf(",", del_1 + 1);
      del_3 = data_packet.indexOf(",", del_2 + 1);
      del_4 = data_packet.indexOf(",", del_3 + 1);
      del_5 = data_packet.indexOf(",", del_4 + 1);
      // Convert and store the received data items.
      air_quality.no2 = data_packet.substring(del_1 + 1, del_2).toFloat();
      air_quality.ozone = data_packet.substring(del_2 + 1, del_3).toInt();
      air_quality.temperature = data_packet.substring(del_3 + 1, del_4).toFloat();
      air_quality.humidity = data_packet.substring(del_4 + 1, del_5).toFloat();
      air_quality.wind_speed = data_packet.substring(del_5 + 1).toInt();
      Serial.println("\nData parameters obtained and saved successfully!\n");
    }
    // Clear the incoming data packet.
    delay(1000);
    data_packet = "";
  }

  // Every 5 minutes, run the Edge Impulse model to make predictions on the air quality levels (classes).
  // If manual testing is required, FireBeetle ESP32 can also run an inference when the built-in button is pressed.
  if((millis() - model_timer > 300000) || !digitalRead(button)){
    // Run inference:
    run_inference_to_make_predictions(1);
    // If the Edge Impulse model predicts an air quality level (class) successfully:
    if(predicted_class != -1){
      // Create the request string.
      String request = "?no2=" + String(air_quality.no2)
                     + "&o3=" + String(air_quality.ozone)
                     + "&temperature=" + String(air_quality.temperature)
                     + "&humidity=" + String(air_quality.humidity)
                     + "&wind_speed=" + String(air_quality.wind_speed)
                     + "&model_result=" + classes[predicted_class];
      // Capture a picture with the OV7725 camera.               
      take_picture(true);
      // Send the obtained data parameters, the recently captured image, and the model detection result to the web application via an HTTP POST request.
      make_a_post_request(request);
        
      // Clear the predicted label (class).
      predicted_class = -1; 
      // Update the model timer.
      model_timer = millis();
    }
  }
}

void run_inference_to_make_predictions(int multiply){
  // Scale (normalize) data items depending on the given model:
  float scaled_no2 = air_quality.no2;
  float scaled_ozone = float(air_quality.ozone);
  float scaled_temperature = air_quality.temperature;
  float scaled_humidity = air_quality.humidity;
  float scaled_wind_speed = float(air_quality.wind_speed);

  // Copy the scaled data items to the features buffer.
  // If required, multiply the scaled data items while copying them to the features buffer.
  for(int i=0; i<multiply; i++){  
    features[feature_ix++] = scaled_no2;
    features[feature_ix++] = scaled_ozone;
    features[feature_ix++] = scaled_temperature;
    features[feature_ix++] = scaled_humidity;
    features[feature_ix++] = scaled_wind_speed;
  }

  // Display the progress of copying data to the features buffer.
  Serial.print("Features Buffer Progress: "); Serial.print(feature_ix); Serial.print(" / "); Serial.println(EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE);
  
  // Run inference:
  if(feature_ix == EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE){    
    ei_impulse_result_t result;
    // Create a signal object from the features buffer (frame).
    signal_t signal;
    numpy::signal_from_buffer(features, EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, &signal);
    // Run the classifier:
    EI_IMPULSE_ERROR res = run_classifier(&signal, &result, false);
    ei_printf("\nrun_classifier returned: %d\n", res);
    if(res != 0) return;

    // Print the inference timings on the serial monitor.
    ei_printf("Predictions (DSP: %d ms., Classification: %d ms., Anomaly: %d ms.): \n", 
        result.timing.dsp, result.timing.classification, result.timing.anomaly);

    // Obtain the prediction results for each label (class).
    for(size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++){
      // Print the prediction results on the serial monitor.
      ei_printf("%s:\t%.5f\n", result.classification[ix].label, result.classification[ix].value);
      // Get the predicted label (class).
      if(result.classification[ix].value >= threshold) predicted_class = ix;
    }
    Serial.print("\nPredicted Class: "); Serial.println(predicted_class);

    // Detect anomalies, if any:
    #if EI_CLASSIFIER_HAS_ANOMALY == 1
      ei_printf("Anomaly : \t%.3f\n", result.anomaly);
    #endif

    // Clear the features buffer (frame):
    feature_ix = 0;
  }
}

void take_picture(bool _abort){
  // Release the image buffer if the board throws memory allocation errors.
  if(_abort) esp_camera_fb_return(fb);
  // Capture a picture with the OV7725 camera.
  fb = esp_camera_fb_get();
  // If successful:
  if(!fb) {
    Serial.println("\nImage capture failed!");
    delay(1000);
    ESP.restart();
  }
  Serial.print("\nImage captured successfully: "); Serial.println(fb->len);
  delay(500);
}

void make_a_post_request(String request){
  // Connect to the web application named weather_station_data_center. Change '80' with '443' if you are using SSL connection.
  if (client.connect(server, 80)){
    // If successful:
    Serial.println("\nConnected to the web application successfully!\n");
    // Create the query string:
    String query = application + request;
    // Make an HTTP POST request:
    String head = "--EnvNotification\r\nContent-Disposition: form-data; name=\"captured_image\"; filename=\"new_image.txt\"\r\nContent-Type: text/plain\r\n\r\n";
    String tail = "\r\n--EnvNotification--\r\n";
    // Get the total message length.
    uint32_t totalLen = head.length() + fb->len + tail.length();
    // Start the request:
    client.println("POST " + query + " HTTP/1.1");
    client.println("Host: 192.168.1.22");
    client.println("Content-Length: " + String(totalLen));
    client.println("Connection: Keep-Alive");
    client.println("Content-Type: multipart/form-data; boundary=EnvNotification");
    client.println();
    client.print(head);
    client.write(fb->buf, fb->len);
    client.print(tail);
    // Release the image buffer.
    esp_camera_fb_return(fb);
    delay(2000);
    // If successful:
    Serial.println("HTTP POST => Data transfer completed!\n");
  }else{
    Serial.println("\nConnection failed to the web application!\n");
    delay(2000);
  }
}

void save_data_to_CSV(const char * file_path, const char * _data, String f_name){  
  // Create a CSV file on the SD card with the given file name. 
  File file = SD_MMC.open(file_path, FILE_WRITE);
  if(!file){ Serial.println("SD Card: Failed to open the given CSV file!\n"); return; }
  // Append the header and the given data items to the generated CSV file. 
  if(file.print(_data)){ Serial.println("SD Card => Data appended successfully: " + f_name + "\n"); }
  else{ Serial.println("SD Card: Data append failed!\n"); }
}

AIoT_weather_station_sensor_readings.ino

Arduino
         /////////////////////////////////////////////  
        //     AI-assisted Air Quality Monitor     //
       //          w/ IoT Surveillance            //
      //             ---------------             //
     //             (Arduino Mega)              //           
    //             by Kutluhan Aktar           // 
   //                                         //
  /////////////////////////////////////////////

//
// Log NO2, O3, and weather data, train a NN model to detect air pollution, and display real-time results w/ surveillance footage on a PHP web app.
//
// For more information:
// https://www.theamplituhedron.com/projects/AI_assisted_Air_Quality_Monitor_w_IoT_Surveillance
//
//
// Connections
// Arduino Mega :
//                                FireBeetle ESP32
// D18  --------------------------- D4
// D19  --------------------------- D2
//                                DFRobot Gravity: Electrochemical Ozone Sensor
// D20  --------------------------- SDA
// D21  --------------------------- SCL
//                                DFRobot Gravity: Electrochemical Nitrogen Dioxide Sensor
// D20  --------------------------- SDA
// D21  --------------------------- SCL
//                                SH1106 OLED Display (128x64)
// D23  --------------------------- SDA
// D22  --------------------------- SCK
// D24  --------------------------- RST
// D25  --------------------------- DC
// D26  --------------------------- CS   
//                                DHT22 Temperature and Humidity Sensor
// D27  --------------------------- DATA
//                                DFRobot Anemometer Kit
// A0   --------------------------- S (Yellow)
//                                Keyes 10mm RGB LED Module (140C05)
// D2   --------------------------- R
// D3   --------------------------- G
// D4   --------------------------- B
//                                Control Button (A)
// D5   --------------------------- +
//                                Control Button (B)
// D6   --------------------------- +
//                                Control Button (C)
// D7   --------------------------- +


// Include the required libraries:
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH1106.h>
#include "DFRobot_OzoneSensor.h"
#include "DFRobot_MultiGasSensor.h"
#include "DHT.h"

// Define the collect number (Ozone Sensor). The collection range is 1-100: 
#define COLLECT_NUMBER  20 

// To modify the ozone sensor's I2C address, configure the hardware IIC address by the dial switch - A0, A1 (ADDRESS_0 for [0 0]), (ADDRESS_1 for [1 0]), (ADDRESS_2 for [0 1]), (ADDRESS_3 for [1 1]).             
/*
    The default IIC device address is OZONE_ADDRESS_3: 
      OZONE_ADDRESS_0  0x70
      OZONE_ADDRESS_1  0x71
      OZONE_ADDRESS_2  0x72
      OZONE_ADDRESS_3  0x73
*/
#define Ozone_IICAddress  OZONE_ADDRESS_3
// Define the IIC Ozone Sensor.
DFRobot_OzoneSensor Ozone;

// To modify the NO2 sensor's I2C address, configure the hardware IIC address by the dial switch - A0, A1 (0x74 for [0 0]), (0x75 for [1 0]), (0x76 for [0 1]), (0x77 for [1 1]).
/*
    The default IIC device address is 0x74: 
      0x74
      0x75
      0x76
      0x77
*/ 
#define NO2_I2C_ADDRESS  0x74
// Define the IIC Nitrogen Dioxide (NO2) Sensor.
DFRobot_GAS_I2C gas(&Wire, NO2_I2C_ADDRESS);

// Define the SH1106 screen settings:
#define OLED_MOSI      23 // MOSI (SDA)
#define OLED_CLK       22 // SCK
#define OLED_DC        25
#define OLED_CS        26
#define OLED_RESET     24
Adafruit_SH1106 display(OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);

// Define monochrome graphics:
static const unsigned char PROGMEM _error [] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x01, 0x80, 0x01, 0x80,
0x06, 0x00, 0x00, 0x60, 0x0C, 0x00, 0x00, 0x30, 0x08, 0x01, 0x80, 0x10, 0x10, 0x03, 0xC0, 0x08,
0x30, 0x02, 0x40, 0x0C, 0x20, 0x02, 0x40, 0x04, 0x60, 0x02, 0x40, 0x06, 0x40, 0x02, 0x40, 0x02,
0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02,
0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x03, 0xC0, 0x02, 0x40, 0x01, 0x80, 0x02,
0x40, 0x00, 0x00, 0x02, 0x60, 0x00, 0x00, 0x06, 0x20, 0x01, 0x80, 0x04, 0x30, 0x03, 0xC0, 0x0C,
0x10, 0x03, 0xC0, 0x08, 0x08, 0x01, 0x80, 0x10, 0x0C, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x60,
0x01, 0x80, 0x01, 0x80, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x3F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00
};
static const unsigned char PROGMEM _home [] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xC0, 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00,
0x00, 0x40, 0x18, 0x00, 0x00, 0x00, 0xC0, 0x08, 0x00, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x00,
0x80, 0x04, 0x00, 0x00, 0x70, 0x00, 0x04, 0x00, 0x03, 0x86, 0x00, 0x04, 0x00, 0x04, 0x01, 0x00,
0x04, 0x00, 0x08, 0x00, 0x80, 0x08, 0x00, 0x10, 0x00, 0x40, 0x08, 0x00, 0x10, 0x00, 0x78, 0x10,
0x00, 0x10, 0x00, 0x46, 0x00, 0x00, 0x20, 0x00, 0x41, 0x80, 0x00, 0x20, 0x00, 0x00, 0x80, 0x00,
0xE0, 0x00, 0x00, 0x40, 0x01, 0x00, 0x60, 0x00, 0x40, 0x02, 0x00, 0x18, 0x00, 0x20, 0x02, 0x00,
0x08, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0x40, 0x3F, 0xFF, 0xF0,
0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x06,
0x00, 0x00, 0x00, 0x7F, 0xF8, 0x00, 0x07, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
};
static const unsigned char PROGMEM _run [] = {
0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x00, 0x1F, 0xFF, 0xF8, 0x00, 0x00,
0x7F, 0xFF, 0xFC, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0x80, 0x03, 0xFF,
0xFF, 0xFF, 0xC0, 0x07, 0xFF, 0xFF, 0xFF, 0xE0, 0x0F, 0xFF, 0xFF, 0xFF, 0xF0, 0x0F, 0xFF, 0xFF,
0xFF, 0xF0, 0x1F, 0xF3, 0xFF, 0xFF, 0xF8, 0x3F, 0xF8, 0xFF, 0xFF, 0xF8, 0x3F, 0xF8, 0x7F, 0xFF,
0xFC, 0x3F, 0xF8, 0x1F, 0xFF, 0xFC, 0x7F, 0xF8, 0x07, 0xFF, 0xFE, 0x7F, 0xF8, 0x03, 0xFF, 0xFE,
0x7F, 0xF8, 0x00, 0xFF, 0xFE, 0x7F, 0xF8, 0x00, 0x3F, 0xFE, 0x7F, 0xF8, 0x00, 0x0F, 0xFE, 0x7F,
0xF8, 0x00, 0x07, 0xFE, 0x7F, 0xF8, 0x00, 0x07, 0xFE, 0x7F, 0xF8, 0x00, 0x0F, 0xFE, 0x7F, 0xF8,
0x00, 0x3F, 0xFE, 0x7F, 0xF8, 0x00, 0xFF, 0xFE, 0x7F, 0xF8, 0x03, 0xFF, 0xFE, 0x7F, 0xF8, 0x07,
0xFF, 0xFE, 0x3F, 0xF8, 0x1F, 0xFF, 0xFC, 0x3F, 0xF8, 0x7F, 0xFF, 0xFC, 0x3F, 0xF8, 0xFF, 0xFF,
0xF8, 0x1F, 0xF3, 0xFF, 0xFF, 0xF8, 0x0F, 0xFF, 0xFF, 0xFF, 0xF0, 0x0F, 0xFF, 0xFF, 0xFF, 0xF0,
0x07, 0xFF, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xFF, 0xC0, 0x01, 0xFF, 0xFF, 0xFF, 0x80, 0x00,
0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x1F, 0xFF, 0xF8, 0x00, 0x00, 0x03,
0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
};
static const unsigned char PROGMEM _clean [] = {
0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x80, 0x00, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x00,
0x01, 0xFF, 0xC0, 0x00, 0x00, 0x03, 0xFF, 0xE0, 0x00, 0x00, 0x03, 0xFF, 0xFC, 0x00, 0x00, 0x3F,
0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0xFF, 0xFF,
0xFF, 0x80, 0x01, 0xFF, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xFF, 0xC0, 0x07, 0xFF, 0xFF, 0xFF,
0xC0, 0x0F, 0xFF, 0xFF, 0xFF, 0xC0, 0x0F, 0xFF, 0xFF, 0xFF, 0xE0, 0x0F, 0xFF, 0xFF, 0xFF, 0xF0,
0x0F, 0xFF, 0xFF, 0xFF, 0xF0, 0x0F, 0xFF, 0xFC, 0xFF, 0xF0, 0x07, 0xFF, 0xF8, 0x7F, 0xF0, 0x03,
0xF8, 0x1C, 0x7F, 0xE0, 0x01, 0xF1, 0x1C, 0xFE, 0x00, 0x00, 0x00, 0xF8, 0xBC, 0x00, 0x00, 0x00,
0x79, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x3C,
0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00,
0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00,
0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x00,
0x1F, 0x7F, 0x70, 0x00, 0x00, 0x40, 0x67, 0x0C, 0x00, 0x00, 0x00, 0xC1, 0x00, 0x00, 0x00, 0x01,
0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const unsigned char PROGMEM _risky [] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x0E, 0x78, 0x00, 0x00, 0x00, 0x7F, 0x1C,
0x00, 0x00, 0x00, 0xFF, 0xEE, 0x00, 0x00, 0x00, 0x7F, 0xE7, 0x80, 0x00, 0x00, 0x07, 0xF3, 0x80,
0x00, 0x00, 0x00, 0x78, 0x60, 0x00, 0x00, 0x00, 0x3F, 0x74, 0x00, 0x00, 0x00, 0x26, 0x7C, 0x00,
0x00, 0x00, 0x07, 0x04, 0x00, 0x00, 0x00, 0x01, 0x86, 0x00, 0x00, 0x00, 0x01, 0x86, 0x00, 0x00,
0x00, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xC7, 0x80, 0x00, 0x00,
0x01, 0xC7, 0x80, 0x00, 0x00, 0x01, 0xC7, 0x80, 0x00, 0x02, 0x03, 0xC7, 0x80, 0x00, 0x02, 0x03,
0xC7, 0x80, 0x00, 0x02, 0x0B, 0xE7, 0x80, 0x00, 0x02, 0x53, 0xE7, 0x80, 0x00, 0x07, 0xFF, 0xEF,
0x80, 0x00, 0x0F, 0xFF, 0xFF, 0xE0, 0x00, 0x0F, 0xFF, 0xFF, 0xE0, 0x00, 0x08, 0x08, 0x3F, 0xE0,
0x00, 0x08, 0x08, 0x3F, 0xE0, 0x00, 0x0F, 0xFF, 0xFF, 0xE0, 0x00, 0x08, 0x08, 0x3F, 0xE0, 0x00,
0x08, 0x08, 0x3F, 0xFF, 0xF0, 0x0F, 0xFF, 0xFF, 0xE0, 0x08, 0x0F, 0xFF, 0xFF, 0xE0, 0x08, 0x1F,
0xFF, 0xFF, 0xE0, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const unsigned char PROGMEM _unhealthy [] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFE, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00,
0x07, 0xFF, 0xC0, 0x00, 0x00, 0x0F, 0xFF, 0xE0, 0x00, 0x00, 0x1F, 0xFF, 0xF0, 0x00, 0x03, 0xFF,
0xFF, 0xF0, 0x00, 0x0F, 0xFF, 0xFF, 0xF8, 0x00, 0x1F, 0xFF, 0xFF, 0xF8, 0x00, 0x1F, 0xFF, 0xFF,
0xF9, 0xC0, 0x3F, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xFF,
0xFC, 0x7F, 0xFF, 0x80, 0xFF, 0xFE, 0x7F, 0xFE, 0x00, 0x7F, 0xFE, 0x7F, 0xF8, 0x00, 0x1F, 0xFE,
0x7F, 0xF8, 0x3C, 0x0F, 0xFE, 0x7F, 0xF0, 0xFF, 0x0F, 0xFF, 0x7F, 0xE1, 0xFF, 0x87, 0xFF, 0x7F,
0xE3, 0xFF, 0xC3, 0xFE, 0x7F, 0xC7, 0xFF, 0xE3, 0xFE, 0x3F, 0xC7, 0xFF, 0xE3, 0xFE, 0x3F, 0x8F,
0xFF, 0xF1, 0xFE, 0x1F, 0x8F, 0xFF, 0xF1, 0xFC, 0x0F, 0x8F, 0xFF, 0xF1, 0xF8, 0x07, 0x8F, 0x3C,
0x71, 0xF0, 0x03, 0x8E, 0x3C, 0x71, 0xE0, 0x00, 0x0E, 0x18, 0x30, 0x00, 0x00, 0x0E, 0x1C, 0x30,
0x00, 0x00, 0x0E, 0x3C, 0x70, 0x00, 0x00, 0x0F, 0xFF, 0xF0, 0x00, 0x00, 0x07, 0xFF, 0xF0, 0x00,
0x00, 0x07, 0xFF, 0xE0, 0x00, 0x00, 0x03, 0xFF, 0xE0, 0x00, 0x00, 0x03, 0xFF, 0xC0, 0x00, 0x00,
0x01, 0xFF, 0xC0, 0x00, 0x00, 0x03, 0x99, 0xC0, 0x00, 0x00, 0x03, 0x99, 0xC0, 0x00, 0x00, 0x01,
0x9D, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
};

// Define the air quality level (class) names and color codes.
String classes[] = {"Clean", "Risky", "Unhealthy"};
int color_codes[3][3] = {{0,255,0}, {255,255,0}, {255,0,0}};

// Define the DHT22 temperature and humidity sensor settings and the DHT object.
#define DHTPIN 27
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

// Define the anemometer kit's voltage signal pin (yellow).
#define anemometer_signal A0

// Define the RGB LED pins:
#define redPin     2
#define greenPin   3
#define bluePin    4

// Define the control button pins:
#define button_A   5
#define button_B   6
#define button_C   7

// Define the data holders: 
unsigned long timer = 0, data_timer = 0;
volatile boolean heating = true;
int ozoneConcentration, wind_speed;
float no2Concentration, humidity, temperature, hic;
String data_packet = "";

void setup(){
  Serial.begin(115200);

  // Initialize the hardware serial port (Serial1) to communicate with FireBeetle ESP32.
  Serial1.begin(115200);

  pinMode(button_A, INPUT_PULLUP);
  pinMode(button_B, INPUT_PULLUP);
  pinMode(button_C, INPUT_PULLUP);
  pinMode(redPin, OUTPUT);
  pinMode(greenPin, OUTPUT);
  pinMode(bluePin, OUTPUT);
  adjustColor(0,0,0);

  // Initialize the SH1106 screen:
  display.begin(SH1106_SWITCHCAPVCC);
  display.display();
  delay(1000);
  
  // Check the IIC Ozone Sensor connection status.
  while(!Ozone.begin(Ozone_IICAddress)){
    Serial.println("IIC Ozone Sensor is not found!\n");
    err_msg();
    delay(1000);
  }
  Serial.println("IIC Ozone Sensor is connected successfully!\n");
  
  /*   
     Set IIC Ozone Sensor mode:
       MEASURE_MODE_AUTOMATIC    // active  mode
       MEASURE_MODE_PASSIVE      // passive mode
  */
  Ozone.setModes(MEASURE_MODE_PASSIVE);
  delay(2000);

  // Check the IIC NO2 Sensor connection status.
  while(!gas.begin()){
    Serial.println("IIC NO2 Sensor is not found!\n");
    err_msg();
    delay(1000);
  }
  Serial.println("IIC NO2 Sensor is connected successfully!\n");

  // Define the IIC NO2 Sensor's data-obtaining mode.
  gas.changeAcquireMode(gas.PASSIVITY);
  delay(1000);
  // Turn on the temperature compensation for the IIC NO2 Sensor.
  gas.setTempCompensation(gas.ON);

  // Initialize the DHT22 sensor.
  dht.begin();

  // If successful:  
  display.clearDisplay();   
  display.setTextSize(2); 
  display.setTextColor(BLACK, WHITE);
  display.setCursor(0,0);
  display.println("AIoT");
  display.println("AirQuality");
  display.println("Monitor");
  display.display();
  delay(1000);
  adjustColor(0,0,255);
}

void loop(){
  // Wait until electrochemical gas sensors heat for 3 minutes.
  if(heating){ timer = millis(); Serial.print("Heating: "); }
  while(millis() - timer < 180000){ if(millis()-timer > 1000){ Serial.print("*"); data_timer = millis(); } }
  heating = false;
  
  collect_air_quality_data();

  home_screen();

  // If one of the control buttons (A, B, or C) is pressed, send the generated data record with the selected air quality level
  // to FireBeetle ESP32 via serial communication.
  if(!digitalRead(button_A)){ Serial1.print("Save&"+data_packet+"&Clean"); data_screen(0); }
  if(!digitalRead(button_B)){ Serial1.print("Save&"+data_packet+"&Risky"); data_screen(1); }
  if(!digitalRead(button_C)){ Serial1.print("Save&"+data_packet+"&Unhealthy"); data_screen(2); }

  // Every minute, transmit the collected air quality data parameters to FireBeetle ESP32 via serial communication.
  if(millis() - data_timer > 60000){ Serial1.print("Data,"+data_packet); run_screen(); data_timer = millis(); }
}

void collect_air_quality_data(){
  // Collect the nitrogen dioxide (NO2) concentration.
  String gastype = gas.queryGasType();
  no2Concentration = gas.readGasConcentrationPPM();
  Serial.print("Ambient " + gastype + " Concentration => "); Serial.print(no2Concentration); Serial.println(" PPM");
  delay(1000);

  // Collect the ozone (O3) concentration.
  ozoneConcentration = Ozone.readOzoneData(COLLECT_NUMBER);
  Serial.print("Ambient Ozone Concentration => "); Serial.print(ozoneConcentration); Serial.println(" PPB");
  delay(1000);

  // Collect the data generated by the DHT22 sensor.
  humidity = dht.readHumidity();
  temperature = dht.readTemperature(); // Celsius
  // Compute the heat index in Celsius (isFahreheit = false).
  hic = dht.computeHeatIndex(temperature, humidity, false);
  Serial.print(F("\nHumidity: ")); Serial.print(humidity); Serial.println("%");
  Serial.print(F("Temperature: ")); Serial.print(temperature); Serial.println(" C");
  Serial.print("Heat Index: "); Serial.print(hic); Serial.println(" C");
  delay(1000);

  // Collect the data generated by the anemometer kit.
  float outvoltage = analogRead(anemometer_signal) * (5.0 / 1023.0);
  // Calculate the wind speed (level) [1 - 30] according to the output voltage.
  wind_speed = 6 * outvoltage;
  Serial.print("Wind Speed (Level) => "); Serial.println(wind_speed); Serial.print("\n");

  // Combine all data items to create a data record.
  data_packet = String(no2Concentration) + ","
              + String(ozoneConcentration) + ","
              + String(temperature) + ","
              + String(humidity) + ","
              + String(wind_speed);
}

void home_screen(){
  display.clearDisplay();   
  display.drawBitmap((128 - 40), 0, _home, 40, 40, WHITE);
  display.setTextSize(1); 
  display.setTextColor(WHITE);
  display.setCursor(0,5);
  display.print("NO2: "); display.print(no2Concentration); display.println(" PPM");
  display.print("O3:  "); display.print(ozoneConcentration); display.println(" PPB\n");
  display.print("Tem: "); display.print(temperature); display.println(" *C");
  display.print("Hum: "); display.print(humidity); display.println("%");
  display.print("Wind: "); display.println(wind_speed);
  display.display();
  delay(100);
}

void data_screen(int i){
  display.clearDisplay(); 
  if(i==0) display.drawBitmap((128 - 40) / 2, 0, _clean, 40, 40, WHITE);
  if(i==1) display.drawBitmap((128 - 40) / 2, 0, _risky, 40, 40, WHITE);
  if(i==2) display.drawBitmap((128 - 40) / 2, 0, _unhealthy, 40, 40, WHITE);
  // Print:
  int str_x = classes[i].length() * 11;
  display.setTextSize(2); 
  display.setTextColor(WHITE);
  display.setCursor((128 - str_x) / 2, 48);
  display.println(classes[i]);
  display.display();
  adjustColor(color_codes[i][0], color_codes[i][1], color_codes[i][2]);
  delay(2000);
  adjustColor(0,0,0);
}

void run_screen(){
  display.clearDisplay(); 
  display.drawBitmap((128 - 40) / 2, 0, _run, 40, 40, WHITE);
  // Print:
  display.setTextSize(1); 
  display.setTextColor(WHITE);
  display.setCursor(0, 48);
  display.println("Data transferred to");
  display.println("FireBeetle ESP32!");
  display.display();
  adjustColor(255,0,255);
  delay(2000);
  adjustColor(0,0,0);
}

void err_msg(){
  // Show the error message on the SH1106 screen.
  display.clearDisplay();   
  display.drawBitmap(48, 0, _error, 32, 32, WHITE);
  display.setTextSize(1); 
  display.setTextColor(WHITE);
  display.setCursor(0,40); 
  display.println("Check the serial monitor to see the error!");
  display.display(); 
  adjustColor(255,0,0);
  delay(1000);
  display.invertDisplay(true);
  delay(1000);
  display.invertDisplay(false);
  delay(1000);
  adjustColor(0,0,0);
}

void adjustColor(int r, int g, int b){
  analogWrite(redPin, (255-r));
  analogWrite(greenPin, (255-g));
  analogWrite(bluePin, (255-b));
}

bmp_converter.py

Python
from PIL import Image
from glob import glob

# Obtain all raw images transferred by FireBeetle ESP32 as text (.txt) files.
path = "<_enter_path_>\\weather_station_data_center\\env_notifications"
images = glob(path + "/*.txt")

# Convert each text (TXT) file to a JPG file and save the generated JPG files to the images folder.
for img in images:
    loc = path + "/images/" + img.split("\\")[8].split(".")[0] + ".jpg"
    raw = open(img, 'rb').read()
    size = (320,240)
    file = Image.frombuffer('L', size, raw, 'raw', 'L', 0, 1)
    file.save(loc)
    #print("Converted: " + loc)

class.php

PHP
<?php

// Define the _main class and its functions:
class _main {
	public $conn;
	
	public function __init__($conn){
		$this->conn = $conn;
	}
	
    // Database -> Insert Air Quality Data:
	public function insert_new_data($date, $no2, $o3, $wind_speed, $temperature, $humidity, $img, $model_result){
		$sql_insert = "INSERT INTO `entries`(`date`, `no2`, `o3`, `wind_speed`, `temperature`, `humidity`, `img`, `model_result`) 
		               VALUES ('$date', '$no2', '$o3', '$wind_speed', '$temperature', '$humidity', '$img', '$model_result');"
			          ;
		if(mysqli_query($this->conn, $sql_insert)){ return true; } else{ return false; }
	}
	
	// Retrieve all data records from the database table, transmitted by FireBeetle ESP32.
	public function get_data_records(){
		$date=[]; $no2=[]; $o3=[]; $temp=[]; $humd=[]; $wind=[]; $img=[]; $m_result=[];
		$sql_data = "SELECT * FROM `entries` ORDER BY `id` DESC";
		$result = mysqli_query($this->conn, $sql_data);
		$check = mysqli_num_rows($result);
		if($check > 0){
			while($row = mysqli_fetch_assoc($result)){
				array_push($date, $row["date"]);
				array_push($no2, $row["no2"]);
				array_push($o3, $row["o3"]);
				array_push($temp, $row["temperature"]);
				array_push($humd, $row["humidity"]);
				array_push($wind, $row["wind_speed"]);
				array_push($img, $row["img"]);
				array_push($m_result, $row["model_result"]);
			}
			return array($date, $no2, $o3, $temp, $humd, $wind, $img, $m_result);
		}else{
			return array(["Not Found!"], ["Not Found!"], ["Not Found!"], ["Not Found!"], ["Not Found!"], ["Not Found!"], ["surveillance.jpg"], ["Not Found!"]);
		}
	}
}

// Define database and server settings:
$server = array(
	"name" => "localhost",
	"username" => "root",
	"password" => "",
	"database" => "air_quality_aiot"
);

$conn = mysqli_connect($server["name"], $server["username"], $server["password"], $server["database"]);

update_data.php

PHP
<?php

include_once "assets/class.php";

// Define the new 'air' object:
$air = new _main();
$air->__init__($conn);

# Get the current date and time.
$date = date("Y_m_d_H_i_s");

# Create the image file name. 
$img_file = "IMG_".$date;

// If FireBeetle ESP32 sends the collected air quality data parameters with the model detection result, save the received information to the given MySQL database table.
if(isset($_GET["no2"]) && isset($_GET["o3"]) && isset($_GET["wind_speed"]) && isset($_GET["temperature"]) && isset($_GET["humidity"]) && isset($_GET["model_result"])){
	if($air->insert_new_data($date, $_GET["no2"], $_GET["o3"], $_GET["wind_speed"], $_GET["temperature"], $_GET["humidity"], $img_file.".jpg", $_GET["model_result"])){
		echo "Air Quality Data Saved to the Database Successfully!";
	}else{
		echo "Database Error!";
	}
}

// If FireBeetle ESP32 transfers a surveillance image (footage) to update the server, save the received raw image as a TXT file to the env_notifications folder.
if(!empty($_FILES["captured_image"]['name'])){
	// Image File:
	$captured_image_properties = array(
	    "name" => $_FILES["captured_image"]["name"],
	    "tmp_name" => $_FILES["captured_image"]["tmp_name"],
		"size" => $_FILES["captured_image"]["size"],
		"extension" => pathinfo($_FILES["captured_image"]["name"], PATHINFO_EXTENSION)
	);
	
    // Check whether the uploaded file extension is in the allowed file formats.
	$allowed_formats = array('jpg', 'png', 'txt');
	if(!in_array($captured_image_properties["extension"], $allowed_formats)){
		echo 'FILE => File Format Not Allowed!';
	}else{
		// Check whether the uploaded file size exceeds the 5 MB data limit.
		if($captured_image_properties["size"] > 5000000){
			echo "FILE => File size cannot exceed 5MB!";
		}else{
			// Save the uploaded file (image).
			move_uploaded_file($captured_image_properties["tmp_name"], "./env_notifications/".$img_file.".".$captured_image_properties["extension"]);
			echo "FILE => Saved Successfully!";
		}
	}
}

// Convert the recently saved raw image (TXT file) to a JPG file via the bmp_converter.py file.
$convert = shell_exec('python "C:\Users\kutlu\New E\xampp\htdocs\weather_station_data_center\env_notifications\bmp_converter.py"');
print($convert);

// After generating the JPG file, remove the recently saved TXT file from the server.
unlink("./env_notifications/".$img_file.".txt");

?>

show_records.php

PHP
<?php

include_once "assets/class.php";

// Define the new 'air' object:
$air = new _main();
$air->__init__($conn);

// Obtain all data records from the database table and print them as table rows.
$date=[]; $no2=[]; $o3=[]; $temp=[]; $humd=[]; $wind=[]; $img=[]; $m_result=[];
list($date, $no2, $o3, $temp, $humd, $wind, $img, $m_result) = $air->get_data_records();
$records = "<tr><th>Date</th><th>NO2</th><th>O3</th><th>Temperature</th><th>Humidity</th><th>Wind Speed</th><th>Model Prediction</th><th>IMG</th></tr>";
for($i=0; $i<count($date); $i++){
	$records .= '<tr class="'.$m_result[$i].'">
				  <td>'.$date[$i].'</td>
				  <td>'.$no2[$i].'</td>
				  <td>'.$o3[$i].'</td>
				  <td>'.$temp[$i].'</td>
				  <td>'.$humd[$i].'</td>
				  <td>'.$wind[$i].'</td>
				  <td>'.$m_result[$i].'</td>
				  <td><button id="env_notifications/images/'.$img[$i].'">I</button></td>
			    </tr>
			   ';   
}	

// Get the latest surveillance image from the database table.
$latest_img = $img[0];

// Create a JSON object from the generated table rows and the latest surveillance image.
$result = array("records" => $records, "latest_img" => "env_notifications/images/".$latest_img);
$res = json_encode($result);

// Return the recently generated JSON object.
echo($res);

?>

index.php

PHP
<!DOCTYPE html>
<html>
<head>
<title>AI-assisted Air Quality Monitor</title>

<!--link to index.css-->
<link rel="stylesheet" type="text/css" href="assets/index.css"></link>

<!--link to favicon-->
<link rel="icon" type="image/png" sizes="36x36" href="assets/icon.png">

<!-- link to FontAwesome-->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v6.2.1/css/all.css">
 
<!-- link to font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display+SC:ital@1&display=swap" rel="stylesheet">

<!--link to jQuery script-->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>

</head>
<body>
<?php ini_set('display_errors',1);?> 
<h1><i class="fa-solid fa-lungs"></i> AI-assisted Air Quality Monitor</h1>
<div class="data">
<table>
<tr><th>Date</th><th>NO2</th><th>O3</th><th>Temperature</th><th>Humidity</th><th>Wind Speed</th><th>Model Prediction</th><th>IMG</th></tr>
<tr><td>X</td><td>X</td><td>X</td><td>X</td><td>X</td><td>X</td><td>X</td><td>X</td></tr>
</table>
</div>

<div class="surveillance">
<section>
<img id="latest_img" src="env_notifications/images/surveillance.jpg" alt="latest_surveillance_image"/>
<figcaption>Latest Surveillance IMG</figcaption>
</section>
<section>
<img id="selected_img" src="env_notifications/images/surveillance.jpg" alt="selected_surveillance_image"/>
<figcaption>Selected Surveillance Image</figcaption>
</section>
</div>

<!--Add the index.js file-->
<script type="text/javascript" src="assets/index.js"></script>

</body>
</html>

index.js

JavaScript
// Display the selected surveillance image (footage) on the screen.
$(".data").on("click", "button", (event) => {
	$("#selected_img").attr('src', event.target.id);
});

// Every 5 seconds, retrieve the HTML table rows generated from the database table rows to inform the user of the latest model detection results on ambient air quality.
setInterval(function(){
	$.ajax({
		url: "./show_records.php",
		type: "GET",
		success: (response) => {
			// Decode the obtained JSON object.
			const res = JSON.parse(response);
			// Assign HTML table rows.
			$(".data table").html(res.records);
			// Assign the latest surveillance image (footage).
			$("#latest_img").attr('src', res.latest_img);
		}
	});
}, 5000);

index.css

CSS
html{background-image:url('background.jpg');background-repeat:no-repeat;background-attachment:fixed;background-size:100% 100%;font-family: 'Playfair Display SC', serif;}
h1{text-align:left;font-weight:bold;padding-left:15px;user-select:none;background:-webkit-linear-gradient(grey, black);-webkit-background-clip:text;-webkit-text-fill-color:transparent;}


.data{position:fixed;bottom:15px;left:15px;width:50%;height:45%;background-color:rgba(54, 60, 50, 0.8);overflow-y:auto;border:10px solid rgba(255, 255, 255, 0.4);border-radius:20px;padding:5px;}
.data table{width:95%;color:white;margin:auto;border:3px solid #EDC591;user-select:none;}
.data th, .data td{border:3px solid #EDC591;color:#1F2020;}
.data th{background-color:#F3D060;}
.data td{color:white;padding:10px;}
.data button{display:block;background-image:linear-gradient(45deg, grey, #F3D060);width:30px;height:30px;font-size:10px;border:2px solid white;border-radius:15px;font-weight:bold;color:white;}
.data button:hover{cursor:pointer;background-image:linear-gradient(45deg, #F3D060, #F3D060);}

.Clean{background-color:#364935;}
.Risky{background-color:#4E5851;}
.Unhealthy{background-color:#A50011;}

.surveillance{position:fixed;top:15px;right:15px;width:350px;height:auto;background-color:rgba(13, 51, 70, 0.6);border:10px solid rgba(255, 255, 255, 0.4);border-radius:20px;padding:5px;}
.surveillance img{display:block;width:320px;height:240px;margin:auto;border:3px solid #EDC591;border-radius:5px;padding:5px;transition:1s;}
.surveillance img:hover{cursor:crosshair;border:3px solid orange;padding:10px;transition:1s;}
.surveillance figcaption{display:block;margin-bottom:10px;margin-top:5px;text-align:center;color:#EDC591;transition:1s;}
.surveillance img:hover ~ figcaption{color:orange;transition:1s;}

/* Width */
::-webkit-scrollbar {width:5px;height:5px;}
/* Track */
::-webkit-scrollbar-track {background-color:#0D3346;}
/* Button */
::-webkit-scrollbar-button{background-color:#EDC591;height:5x;width:5px;}
::-webkit-scrollbar-button:hover{background-color:white;}
/* Handle */
::-webkit-scrollbar-thumb {background-color:#F3D060;}
::-webkit-scrollbar-thumb:hover {background-color:white;}
/* Corner */
::-webkit-scrollbar-corner{background-color:#4d79ff;}

Credits

Kutluhan Aktar

Kutluhan Aktar

80 projects • 299 followers
AI & Full-Stack Developer | @EdgeImpulse | @Particle | Maker | Independent Researcher

Comments