Exam 2 - xyzen/CS490 GitHub Wiki

Project 2: In this exam, we gathered data from various sensors connected to the Arduino Uno, and passed their readings along to node-red on the Raspberry Pi for visualization in node-red-dashboard, as well as sharing via Twitter and email. We chose to use a Python3 script (Exam2.py) to interface between the Arduino and node-red by parsing lines from the serial connection and saving flagged values to a CSV file being watched by node-red, because this approach allowed us to easily separate the code for setting up, calibrating, and reading sensors (on the Arduino) from the node-red interface (on the Raspberry Pi).

Contributions

  • Dylan wrote the code for the Arduino and the Python script to extract values, and helped with the wiki
  • Levi created this wiki, and built the node-red flow for node-red dashboard, Twitter and email
  • Tyler wired up the sensors, integrated and ran the code, and created the demo video.

Physical Setup

Physical components

  • First, the following sketch was uploaded to the Arduino in order to set up, calibrate, and obtain readings from the sensors and print them to serial with a designated flag ('MSG'):

Arduino Code

// Ultrasonic sensor variables
const int trigPin = 12;
const int echoPin = 13;
long sonic_duration;

// MQ135 variables
/************************Hardware Related Macros************************************/
#define         MQ135PIN                       (3)     //define which analog input channel you are going to use
#define         RL_VALUE_MQ135                 (1)     //define the load resistance on the board, in kilo ohms
#define         RO_CLEAN_AIR_FACTOR_MQ135      (3.59)  //RO_CLEAR_AIR_FACTOR=(Sensor resistance in clean air)/RO,
                                                       //which is derived from the chart in datasheet
/***********************Software Related Macros************************************/
#define         CALIBARAION_SAMPLE_TIMES     (50)    //define how many samples you are going to take in the calibration phase
#define         CALIBRATION_SAMPLE_INTERVAL  (500)   //define the time interal(in milisecond) between each samples in the
                                                     //cablibration phase
#define         READ_SAMPLE_INTERVAL         (50)    //define how many samples you are going to take in normal operation
#define         READ_SAMPLE_TIMES            (5)     //define the time interal(in milisecond) between each samples in 
                                                     //normal operation
/**********************Application Related Macros**********************************/
#define         GAS_CARBON_DIOXIDE           (9)
#define         GAS_CARBON_MONOXIDE          (3)
#define         GAS_ALCOHOL                  (4)
#define         GAS_AMMONIUM                 (10)
#define         GAS_TOLUENE                  (11)
#define         GAS_ACETONE                  (12)
//#define         accuracy                     (0)   //for linearcurves
#define         accuracy                   (1)   //for nonlinearcurves, un comment this line and comment the above line if calculations 
                                                   //are to be done using non linear curve equations
/*****************************Globals***********************************************/
float           Ro = 10;                            //Ro is initialized to 10 kilo ohms

// BME280 (temp/hum/pres) variables
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
Adafruit_BME280 bme;

// Dust variables
int dust_pin = 10;
unsigned long dust_duration;
unsigned long starttime;
unsigned long sampletime_ms = 2000; 
unsigned long lowpulseoccupancy = 0;
float ratio = 0;

// Data variables
float temperature;
float humidity;
float pressure;
int light;
float dust_concentration;
int carbon_dioxide;
int carbon_monoxide;
int distance;

void setup() {
  Serial.begin(9600);
  sonic_setup();
  mq135_setup();
  bme_setup();
  dust_setup();
  light_setup();
}

// Ultrasonic sensor setup
void sonic_setup() {
  pinMode(trigPin, OUTPUT); // Sets the trigPin as an Output
  pinMode(echoPin, INPUT); // Sets the echoPin as an Input
  Serial.println("Passed sonic setup");
}

// MQ135 setup
void mq135_setup() {
  Serial.print("Calibrating...\n");                
  Ro = MQCalibration(MQ135PIN);                     //Calibrating the sensor. Please make sure the sensor is in clean air 
                                                    //when you perform the calibration
  Serial.println("Passed mq135 setup");
}



// BME280 (temp/hum/pres) setup
void bme_setup() {
  unsigned status;
  // default settings
  status = bme.begin(0x76);  
  // You can also pass in a Wire library object like &Wire2
  // status = bme.begin(0x76, &Wire2)
  if (!status) {
      Serial.println("Could not find a valid BME280 sensor, check wiring, address, sensor ID!");
      Serial.print("SensorID was: 0x"); Serial.println(bme.sensorID(),16);
      Serial.print("        ID of 0xFF probably means a bad address, a BMP 180 or BMP 085\n");
      Serial.print("   ID of 0x56-0x58 represents a BMP 280,\n");
      Serial.print("        ID of 0x60 represents a BME 280.\n");
      Serial.print("        ID of 0x61 represents a BME 680.\n");
      while (1) delay(10);
  }
  Serial.println("Passed bme setup");
}

// Dust setup
void dust_setup(){
  pinMode(dust_pin,INPUT);
  starttime = millis(); 
  Serial.println("Passed dust setup");
}

//Light setup
void light_setup(){
  pinMode(A0,INPUT);
  Serial.println("Passed light setup");
}

void loop() {
  bme_loop();
  light_loop();
  dust_loop();
  mq135_loop();
  sonic_loop();
  send_data();

}


// Ultrasonic sensor loop
void sonic_loop() {
  // Clears the trigPin
  digitalWrite(trigPin, LOW);
  delayMicroseconds(2);
  // Sets the trigPin on HIGH state for 10 micro seconds
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);
  // Reads the echoPin, returns the sound wave travel time in microseconds
  sonic_duration = pulseIn(echoPin, HIGH);
  // Calculating the distance
  distance= (sonic_duration*0.034)/2;  //Speed of sound in air  at  standard condition = 0.034cm/µs
}

// MQ135 loop
void mq135_loop() {
  carbon_dioxide = MQGetGasPercentage(MQRead(MQ135PIN)/Ro,GAS_CARBON_DIOXIDE);
  carbon_monoxide = MQGetGasPercentage(MQRead(MQ135PIN)/Ro,GAS_CARBON_MONOXIDE);
}

// BME280 loop
void bme_loop() {
  temperature = bme.readTemperature();
  humidity = bme.readHumidity();
  pressure = (bme.readPressure() / 100.0F);
}

//Dust loop
void dust_loop(){
  dust_duration = pulseIn(dust_pin, LOW);
  lowpulseoccupancy = lowpulseoccupancy+dust_duration;
  if ((millis()-starttime) >= sampletime_ms) //if the sample time = = 30s
  {
    ratio = lowpulseoccupancy/(sampletime_ms*10.0);  
    dust_concentration = float(1.1*pow(ratio,3)-3.8*pow(ratio,2)+520*ratio+0.62);
    delay(2000);
    lowpulseoccupancy = 0;
    starttime = millis();
  }
  delay(2000);
}

//Light sensor loop
void light_loop(){
  light=analogRead(A0);
}

// MQ135 functions
/****************** MQResistanceCalculation ****************************************
Input:   raw_adc - raw value read from adc, which represents the voltage
Output:  the calculated sensor resistance
Remarks: The sensor and the load resistor forms a voltage divider. Given the voltage
         across the load resistor and its resistance, the resistance of the sensor
         could be derived.
************************************************************************************/ 
float MQResistanceCalculation(int raw_adc)
{
  return ( ((float)RL_VALUE_MQ135*(1023-raw_adc)/raw_adc));
}

/***************************** MQCalibration ****************************************
Input:   mq_pin - analog channel
Output:  Ro of the sensor
Remarks: This function assumes that the sensor is in clean air. It use  
         MQResistanceCalculation to calculates the sensor resistance in clean air 
         and then divides it with RO_CLEAN_AIR_FACTOR. RO_CLEAN_AIR_FACTOR is about 
         10, which differs slightly between different sensors.
************************************************************************************/ 
float MQCalibration(int mq_pin)
{
  int i;
  float RS_AIR_val=0,r0;

  for (i=0;i<CALIBARAION_SAMPLE_TIMES;i++) {                     //take multiple samples
    RS_AIR_val += MQResistanceCalculation(analogRead(mq_pin));
    delay(CALIBRATION_SAMPLE_INTERVAL);
  }
  RS_AIR_val = RS_AIR_val/CALIBARAION_SAMPLE_TIMES;              //calculate the average value

  r0 = RS_AIR_val/RO_CLEAN_AIR_FACTOR_MQ135;                      //RS_AIR_val divided by RO_CLEAN_AIR_FACTOR yields the Ro 
                                                                 //according to the chart in the datasheet 

  return r0; 
}

/*****************************  MQRead *********************************************
Input:   mq_pin - analog channel
Output:  Rs of the sensor
Remarks: This function use MQResistanceCalculation to caculate the sensor resistenc (Rs).
         The Rs changes as the sensor is in the different consentration of the target
         gas. The sample times and the time interval between samples could be configured
         by changing the definition of the macros.
************************************************************************************/ 
float MQRead(int mq_pin)
{
  int i;
  float rs=0;

  for (i=0;i<READ_SAMPLE_TIMES;i++) {
    rs += MQResistanceCalculation(analogRead(mq_pin));
    delay(READ_SAMPLE_INTERVAL);
  }

  rs = rs/READ_SAMPLE_TIMES;

  return rs;  
}

/*****************************  MQGetGasPercentage **********************************
Input:   rs_ro_ratio - Rs divided by Ro
         gas_id      - target gas type
Output:  ppm of the target gas
Remarks: This function uses different equations representing curves of each gas to 
         calculate the ppm (parts per million) of the target gas.
************************************************************************************/ 
int MQGetGasPercentage(float rs_ro_ratio, int gas_id)
{ 
  if ( accuracy == 0 ) {
  if ( gas_id == GAS_CARBON_DIOXIDE ) {
    return (pow(10,((-2.890*(log10(rs_ro_ratio))) + 2.055)));
  } else if ( gas_id == GAS_CARBON_MONOXIDE ) {
    return (pow(10,((-3.891*(log10(rs_ro_ratio))) + 2.750)));
  } else if ( gas_id == GAS_ALCOHOL ) {
    return (pow(10,((-3.181*(log10(rs_ro_ratio))) + 1.895)));
  } else if ( gas_id == GAS_AMMONIUM ) {
    return (pow(10,((-2.469*(log10(rs_ro_ratio))) + 2.005)));
  } else if ( gas_id == GAS_TOLUENE ) {
    return (pow(10,((-3.479*(log10(rs_ro_ratio))) + 1.658)));
  } else if ( gas_id == GAS_ACETONE ) {
    return (pow(10,((-3.452*(log10(rs_ro_ratio))) + 1.542)));
  }   
} 

  else if ( accuracy == 1 ) {
    if ( gas_id == GAS_CARBON_DIOXIDE ) {
    return (pow(10,((-2.890*(log10(rs_ro_ratio))) + 2.055)));
  } else if ( gas_id == GAS_CARBON_MONOXIDE ) {
    return (pow(10,(1.457*pow((log10(rs_ro_ratio)), 2) - 4.725*(log10(rs_ro_ratio)) + 2.855)));
  } else if ( gas_id == GAS_ALCOHOL ) {
    return (pow(10,((-3.181*(log10(rs_ro_ratio))) + 1.895)));
  } else if ( gas_id == GAS_AMMONIUM ) {
    return (pow(10,((-2.469*(log10(rs_ro_ratio))) + 2.005)));
  } else if ( gas_id == GAS_TOLUENE ) {
    return (pow(10,((-3.479*(log10(rs_ro_ratio))) + 1.658)));
  } else if ( gas_id == GAS_ACETONE ) {
    return (pow(10,(-1.004*pow((log10(rs_ro_ratio)), 2) - 3.525*(log10(rs_ro_ratio)) + 1.553)));
  }
}    
  return 0;
}

//sends all data to serial
void send_data(){
  String msg = "MSG";
  msg += String(temperature)+"|";
  msg += String(humidity)+"|";
  msg += String(pressure)+"|";
  msg += String(light)+"|";
  msg += String(dust_concentration)+"|";
  msg += String(carbon_dioxide)+"|";
  msg += String(carbon_monoxide)+"|";
  msg += String(distance);
  Serial.println(msg);
}
  • Next, the following Python script is run on the Raspberry Pi in order to read from the serial connection with the Arduino and write the data to the CSV file that is being watched by node-red:

Python Code

#Libraries
import sys
import serial

# Establish serial connection with Arduino
ser = serial.Serial("/dev/ttyACM0", 9600)
ser.baudrate = 9600 #Set baudrate

def get_data():
    read_ser = ser.readline()
    package = read_ser.decode('ASCII').strip()
    if package[:3] == 'MSG':
	# split flagged messages into data entries
        data = package[3:].split("|")
        data = [float(entry) for entry in data]
        print(data)

        # write sensor data to comma-separated-value file
        with open("sensor_data.csv", "w") as file:
            for entry in data[:-1]:
                file.write(str(entry))
                file.write(",")
            file.write(str(data[-1]))
    else:
        print(package)

if __name__ == "__main__":
    while True:
        get_data()
  • Finally, the data is injected into node-red each time the CSV file is updated by Python. Each data value is passed to a separate node-red dashboard chart, while all eight data values are used to construct the "Status" message that is posted to Twitter and sent via email.

Node-Red Flow

node-red flow

Node-Red Dashboard Charts

node-red dashboard charts

Tweets

tweets\

Emails

emails

Link to code

Link to Youtube

Things Learned

Project 2 taught us how to combine all the things we've learned across multiple devices and platforms. We've combined methods of communication, programming languages, and edge devices to create a complete system. From reading sensor data into the Arduino and passing into the Pi; To compiling CSV data and projecting to Twitter, Email, and Node-Red Dashboard; Project 2 provided a complete IoT picture from top to bottom.

⚠️ **GitHub.com Fallback** ⚠️