Telina
Published © GPL3+

Mobile Fine Dust (PM10 & PM2.5) and NO2 Meter

This device measures fine dust and NO2 concentration in the air while on the move and adds GPS coordinates to each measurement location.

AdvancedShowcase (no instructions)12,442
Mobile Fine Dust (PM10 & PM2.5) and NO2 Meter

Things used in this project

Hardware components

Arduino Nano R3
Arduino Nano R3
This component has to be soldered on the main interconnection board or has to be plugged into two 15 pin SIP-sockets.
×1
SDcard reader VMA304
This component has to be soldered on the main interconnection board or has to be plugged into an 8-pin SIP-socket.
×1
Open Smart GPS-module
This module has to be soldered on the main interconnection board or has to be plugged into an 5-pin SIP-socket.
×1
Fine dust sensor SDS011
This sensor can be connected to the main interconnection board by means of the 4-wire flat cable which is delivered with the sensor.
×1
DHT22 Temperature Sensor
DHT22 Temperature Sensor
This sensor has to be connected by a 3-wire flat cable to the main interconnection board. Of the 4 pins only VCC, GND and DATA have to be connected.
×1
NO2-sensor CJMCU-4541
This sensor has to be connected by a 4-wire flat cable to the main interconnection board. Of the 5 pins only VCC, GND, NOX and PREHEAT have to be connected.
×1
555 Timers
555 Timers
This component has to be soldered on the main interconnection board or has to be plugged into an 8-pin DIP-socket.
×1
7-segment single digit
These single digit display units have to be soldered onto the display board.
×6
74HC4511 BCD to Seven-Segment Decoder / Driver / Latch
These TTL ICs have to be soldered onto the display board.
×6
74LS138 3-to-8 line decoder TTL IC
This TTL IC has to be soldered onto the display board.
×1
General Purpose Transistor NPN
General Purpose Transistor NPN
These transistors have to be soldered onto the main interconnection board.
×2
General Purpose Transistor PNP
General Purpose Transistor PNP
This transistor has to be soldered onto the main interconnection board.
×1
Resistor 330 ohm
Resistor 330 ohm
These resistors have to be soldered onto the main interconnection board.
×3
Resistor 560 ohm
This resistor has to be soldered onto the main interconnection board.
×1
Resistor 1k ohm
Resistor 1k ohm
These resistors have to be soldered onto the main interconnection board.
×5
Resistor 2.21k ohm
Resistor 2.21k ohm
This resistor has to be soldered onto the main interconnection board.
×1
3 mm LED: Red
3 mm LED: Red
This LED has to be soldered onto the main interconnection board or (when the display board is used) onto the display board to be visible.
×2
Dual Pole Dual Throw (DPDT) Switch
This switch has to be connected to the main interconnection board.
×1
Resistor 33k ohm
This resistor has to be soldered onto the main interconnection board.
×1
Single Turn Potentiometer- 100k ohms
Single Turn Potentiometer- 100k ohms
This potentiometer has to be soldered onto the main interconnection board. It has to be set to provide a preheating time of about 30 seconds to the NO2-sensor.
×1
Capacitor 100 nF
Capacitor 100 nF
This capacitor has to be soldered onto the main interconnection board.
×1
Capacitor 100 µF
Capacitor 100 µF
This capacitor has to be soldered onto the main interconnection board.
×1

Software apps and online services

Google Maps
Google Maps
To show the measurement results a dynamic HTML-page has been developed using PHP to read all uploaded files and display these as dropdown lists for each configured finedust meter. When a ride from the list is selected a GoogleMap appears on which the measurement results are shown as colored circles and donuts as well as a bar chart on which all measured values are shown as colored bars. Sequence number and time of measurement as well as detailed values are shown when a circle on the map is clicked or when hovering over the colored bars in the bar chart. The measurement results of the four mobile finedust meters, which are in use at this moment, can be consulted via the url www.kriterion.be/GoogleMaps/FineDustResults.php

Story

Read more

Custom parts and enclosures

Transparant housing for mobile finedust meter

This is the housing in which all components will fit perfectly, including the display board and battery.

Mechanical drawing of main interconnection PCB

A printed circuit board has been developed on which all components can be soldered or connected by means of flat cables. The Arduino Nano microcontroller, SDcard, GPS-module and electronics to ensure soft power down are soldered onto this board. DHT-, NO2- and PM-sensors are connected to this board via flat cables.

Schematics

Schematics of the main interconnection board

This board interconnects all components: Arduino Nano, OpenSmart GPS-module and SDcard reader are plugged directly onto the board. The sensors SDS011, DHT22 and GJMCU-4541 are connected via cables as well as the optional display board and the earphone connector for the external warning device. The board houses also the soft power off electronics, a timer to provide a 30 seconds preheating of the NO2-sensor and a LED indicating write cycles to the SDcard. Battery power and DPDT-switch are also connected to the main board.

Display Board

This is the optional display board, which comprises 2x3 7-segment digits to show the measurement results of the 5 sensor values in real time.

Code

Mobile fine dust and NO2 meter

Arduino
This is the code for the mobile fine dust meter with the optional display and NO2 meter, but this code also works for the basic version without display and the NO2 meter.
Mandatory modules are the SDS011 fine dust sensor, the DHT22 sensor for measuring humidity and temperature and the GPS-module, because these modules transmit their measurements over a serial interface and for each of these sensors a wait-loop is traversed to capture new serial data. Also mandatory is the SDcard interface (SPI) which takes care of the logging of the gathered data on a SDcard.
Further, this code features the following functions:
1. Creation of a new logfile each time the meter is powered on.
2. Calculation of a hash code when writing a new data record to the logfile.
3. Creation of a digital signature using the hash code at power shutdown or too low supply voltage.
4. Monitoring of the supply voltage and giving a low battery warning on the display
5. Software controlled power down to finish ongoing write cycle to the SDcard and writing the signature.
6. Round-robin rotation through 4 display modes by toggling the DPDT power switch.
7. Showing 6 different error codes on the display:
001 SDcard initialization failed
002 No FAT partition found
003 Initialization file access failed
004 Low battery voltage
005 GPS signal unreliable
006 Cannot open the logfile
// include the software serial interface library
#include <SoftwareSerial.h>
// include the SDcard library
#include <SPI.h>
#include <SD.h>
// include the Digital Humidity and Temperature (DHT) library
#include <DHT.h>

// set up variables using the SDcard utility library functions
Sd2Card card;
SdVolume volume;

// set up variables using the softserial utility library functions
SoftwareSerial SDS_Serial(8, 9);   // RX, TX (TX unused and assigned as RX-pin for DHT-sensor)
SoftwareSerial GPS_Serial(5, 6);   // RX, TX (TX unused and assigned as output pin for warning signal)

// unique identification code of the finedustmeter (no leading zeroes allowed!)
const unsigned int id_lo = 802; // serial number (unique in each country)
const unsigned int id_hi = 32;  // country code

const byte Select0 = 2;      // Display select pin for bit0
const byte Select1 = 3;      // Display select pin for bit1
const byte Select2 = 4;      // Display select pin for bit2
const byte Warning = 6;      // Output pin to initiate warning signal
const byte Write2LogLED = 7; // Notification that data is written to logfile
const byte DHT_pin = 9;      // Input pin to receive serial DHT-data
const byte chipSelect = 10;  // Select pin to enable access to the SDcard
const byte Data0 = 14;       // Display data pin for bit0
const byte Data1 = 15;       // Display data pin for bit1
const byte Data2 = 16;       // Display data pin for bit2 
const byte Data3 = 17;       // Display data pin for bit3
const byte Latch_data = 18;  // Clock line to latch data into selected register
const byte Power_hold = 19;  // Output pin to switch off meter outside SDcard write cycle
const int NO2_pin = A6;      // Analog PIN 6 to read the NO2-sensor
const int Switch_status = A7;// Analog PIN 7 to sense status of on-off switch and supply voltage
const byte Vmin = 43;        // Minimum supply voltage for writing data to SDcard
static char line[40];        // Linebuffer for serial monitor
char datafile[] = "log_000.txt"; // Initial name of datalogfile
unsigned int filecount = 0;
unsigned int lowpowercount = 0;
unsigned int Pm25 = 0;
unsigned int Pm10 = 0;
uint8_t byteGPS;
uint8_t display_mode = 0; // 0->display PM-values, 1->display NO2 and voltage, 2->display temperature and humidity, 3->display time
uint32_t currentTime = 0;
uint32_t hash = 0;
char cmd[7] = "$GPRMC"; // Recommended minimum specific GPS/Transit data sentence
int counter1 = 0;       // counts how many bytes were received (max 200) for each sentence
int counter2 = 0;       // counts how many commas were seen in each sentence
int offsets[13];        // holds offset to 12 different data items and checksum in GPRMC sentence
char buf[200] = " ";    // data buffer in which each received sentence is stored
float hum;              // stores humidity value
float temp;             // stores temperature value
boolean proceed;
boolean switch_off;
boolean logging_flag;
boolean hash_written;
boolean data_present;
DHT dht(DHT_pin, DHT22); // Initialize DHT-sensor
char digit100(unsigned int v) {return '0' + v / 100 - (v/1000) * 10;}
char digit10(unsigned int v) {return '0' + v / 10 - (v/100) * 10;}
char digit1(unsigned int v) {return '0' + v / 1 - (v/10) * 10;}

void ProcessSerialSDSData() {
  uint8_t mData = 0;
  uint8_t i = 0;
  uint8_t mPkt[10] = {0};
  uint8_t mCheck = 0;
  while (SDS_Serial.available() > 0) { 
    // from www.inovafitness.com
    // packet format: AA C0 PM25_Low PM25_High PM10_Low PM10_High 0 0 CRC AB
    mData = SDS_Serial.read();
    delay(2); //wait until packet is received
    if(mData == 0xAA) { //first headerbyte ok
      mPkt[0] =  mData;
      mData = SDS_Serial.read();
      if(mData == 0xC0) { //second headerbyte ok
        mPkt[1] =  mData;
        mCheck = 0;
        for(i=0;i<6;i++) { //data reception and crc calculation
           mPkt[i+2] = SDS_Serial.read();
           delay(2);
           mCheck += mPkt[i+2];
        }
        mPkt[8] = SDS_Serial.read(); //get crc
        delay(1);
	      mPkt[9] = SDS_Serial.read();
        if(mCheck == mPkt[8]) { //crc ok?
          Pm25 = (uint16_t)mPkt[2] | (uint16_t)(mPkt[3]<<8);
          Pm10 = (uint16_t)mPkt[4] | (uint16_t)(mPkt[5]<<8);
          //one good packet received
          return;
        }
      }      
    }
    Check_power_switch_status();
  } 
}

void reset() {
  counter1 = 0;
  counter2 = 0;
}

void write_nibble(byte regnbr, byte regval) {
  byte i;
  for (i=0; i<4; i++)
  { if ((regval & (1 << i)) == 0) { digitalWrite(Data0 + i, LOW); } else { digitalWrite(Data0 + i, HIGH); } }
  for (i=0; i<3; i++)
  { if ((regnbr & (1 << i)) == 0) { digitalWrite(Select0 + i, LOW); } else { digitalWrite(Select0 + i, HIGH); } }
  digitalWrite(Latch_data, HIGH);
  delay(1);
  digitalWrite(Latch_data, LOW);
}

void write_data(byte regnbr, int regval) {
  if (regval > 999) {
    write_nibble(regnbr, 9);
    write_nibble(regnbr + 1, 9);
    write_nibble(regnbr + 2, 9);
  }
  else {
    byte val_units = regval % 10;
    byte val_tens  = (regval / 10) % 10;
    byte val_hundreds = regval / 100;
    write_nibble(regnbr, val_units);
    if (val_tens == 0 && val_hundreds == 0) {
      write_nibble(regnbr + 1, 15);
    } else {
      write_nibble(regnbr + 1, val_tens);
    }
    if (val_hundreds == 0) {
      write_nibble(regnbr + 2, 15);
    } else {
      write_nibble(regnbr + 2, val_hundreds);
    }
  }
}

void error_idling(byte error_nbr) {
  while(1) {
    write_data(1, 888);
    write_data(4, 888);
    delay(1000);
    write_data(1, error_nbr);
    write_data(4, 0);
    delay(1000);
    Check_power_switch_status();
  }
}

int get_size(int offset) {
  return offsets[offset+1] - offsets[offset] - 1;
}

boolean handle_byte(int byteGPS) {
  uint8_t checksum = 0;
  char str_check[2];
  uint8_t chksum[2];
  buf[counter1] = byteGPS;
  counter1++;
  if (counter1 == 200) {
    return false;
  }
  if (byteGPS == ',') {
    counter2++;
    offsets[counter2] = counter1;
    if (counter2 == 13) {
      return false;
    }
  }
  if (byteGPS == '*') {
    offsets[12] = counter1;
  }
  // Check if we got a <LF>, which indicates the end of line
  if (byteGPS == 10) {
    // Check that we got 12 pieces, and that the first piece is 6 characters
    if (counter2 != 12 || (get_size(0) != 6)) {
      return false;
    }
    // Check that we received $GPRMC
    for (int j=0; j<6; j++) {
      if (buf[j] != cmd[j]) {
        return false;
      }
    }
    // Compute and validate checksum
    for (int j=1; j<offsets[12]-1; j++) {
      checksum ^= buf[j];
    }
    chksum[0] = buf[offsets[12]];
    if (chksum[0] > 47 && chksum[0] < 58) {
      chksum[0] -= 48;
    } else {
      chksum[0] -= 55;
    }
    chksum[1] = buf[offsets[12]+1];
    if (chksum[1] > 47 && chksum[1] < 58) {
      chksum[1] -= 48;
    } else {
      chksum[1] -= 55;
    }
    if (checksum != chksum[0]*16 + chksum[1]) {
      return false;
    }
    proceed = true; // one good sentence received
    return false;
  }
  return true;
}

void calc_hash(char c) {
  hash += (byte)c;
  hash += (hash << 10);
  hash ^= (hash >> 6);
}

void write_file_hash() {
  typedef union {
    uint32_t v;
    unsigned char b[4];
  } short4bytes_t;
  if (SD.exists(datafile)) {
    if (data_present) {
      hash += (hash << 3);
      hash ^= (hash >> 11);
      hash += (hash << 15);
      short4bytes_t s4b;
      s4b.v = hash;
      File dataFile = SD.open(datafile, FILE_WRITE);
      if (dataFile) {
        digitalWrite(Write2LogLED, HIGH);
        sprintf(buf, "Signature: 0x%02X 0x%02X 0x%02X 0x%02X", s4b.b[3], s4b.b[2], s4b.b[1], s4b.b[0]);
        dataFile.println(buf);
        dataFile.close();
        digitalWrite(Write2LogLED, LOW);
      }
    }
    else {
      SD.remove(datafile);
    }
  }
}

void Check_power_switch_status() {
  // when in off state more than 1 second, disable logging, write hash to SDcard and shutdown
  // when toggled to on state within 1 second, switch to next display mode
  uint16_t v = analogRead(Switch_status);
  if (v < 100 && !switch_off) {
    currentTime = millis();
    switch_off = true;
    delay(200);
  }
  if (v < 100 && switch_off && abs(millis() - currentTime) > 2000 && !hash_written) {
    logging_flag = false;
    write_file_hash();
    hash_written = true;
    delay(1000);
    digitalWrite(Power_hold, LOW);
  }
  if (v > 100 && switch_off) {
    switch_off = false;
    ++display_mode %= 4;
    delay(200);
    write_data(1, display_mode);
    for (byte i=4; i<7; i++) write_nibble(i, 15);
    delay(1000);
  }
}

byte hour_inc(int date_offset) {
  byte year;
  byte month;
  byte day;
  byte i;
  char datebuf[3];
  uint8_t days[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30 }; // nbr of days to be added from prev month at beginning of each month
  uint8_t hours[] = { 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1 }; // nbr of hours to be added to GMT for daylight saving time in GMT+1 timezone
  datebuf[2] = '\0';
  for (i=0; i<2; i++) datebuf[i] = buf[date_offset+i+2];
  month = atoi(datebuf) - 1; // decrement month value from GPRMC sentence, since days and hours arrays range from 0 to 11
  if (month == 2 || month == 9) {
    for (i=0; i<2; i++) datebuf[i] = buf[date_offset+i+4];
    year = atoi(datebuf);
    for (i=0; i<2; i++) datebuf[i] = buf[date_offset+i];
    day = atoi(datebuf);
    // calculate number of days at 1/3 or 1/10 since monday, 1 january 2018
    uint16_t nbr_days = (year - 18)*365;
    for (i=0; i<=month; i++) nbr_days += days[i];
    // correction for leap years
    if (year > 20) nbr_days += (year - 21)/4 + 1;
    if (year % 4 == 0) nbr_days++;
    // find date of last sunday in march or october
    uint8_t last_sunday = 31;
    while ((nbr_days + last_sunday) % 7 > 0) last_sunday--;
    // determine changeover from wintertime to summertime and vice versa
    if (month == 2) { if (day < last_sunday) return 1; else return 2; }
    if (month == 9) { if (day < last_sunday) return 2; else return 1; }
  } else {
    return hours[month];
  }  
}

void setup() {
  pinMode(Data0, OUTPUT);
  pinMode(Data1, OUTPUT);
  pinMode(Data2, OUTPUT);
  pinMode(Data3, OUTPUT);
  pinMode(Select0, OUTPUT);
  pinMode(Select1, OUTPUT);
  pinMode(Select2, OUTPUT);
  pinMode(Latch_data, OUTPUT);
  pinMode(Power_hold, OUTPUT);
  pinMode(Write2LogLED, OUTPUT);
  digitalWrite(Warning, LOW);
  digitalWrite(Power_hold, LOW);
  digitalWrite(Latch_data, LOW);
  analogReference(EXTERNAL); // Aref must be connected to 3.3V pin
  // start serial connection to SDS-module
  SDS_Serial.begin(9600);
  // start serial connection to GPS-module
  GPS_Serial.begin(9600);
  Serial.begin(9600);
  dht.begin();
  delay(500); // wait for display to boot up
  // put unique identification code on display
  write_data(1, id_lo);
  write_data(4, id_hi);
  strcpy_P(line,PSTR("Initializing SD card..."));
  Serial.println(line);
  if (!card.init(SPI_HALF_SPEED, chipSelect)) {
    strcpy_P(line,PSTR("Initialization failed."));
    Serial.println(line);
    error_idling(1);
  } else {
    strcpy_P(line,PSTR("SD card is OK.  "));
    Serial.println(line);
  }
  // print the type of the SDcard
  strcpy_P(line,PSTR("Card type: "));
  Serial.print(line);
  switch (card.type()) {
    case SD_CARD_TYPE_SD1:
      Serial.println("SD1");
      break;
    case SD_CARD_TYPE_SD2:
      Serial.println("SD2");
      break;
    case SD_CARD_TYPE_SDHC:
      Serial.println("SDHC");
      break;
    default:
      Serial.println("?");
  }
  delay(2000); // allow some time (2 seconds) to read init result
  // try to open the 'volume'/'partition' - it should be FAT16 or FAT32
  if (!volume.init(card)) {
    strcpy_P(line,PSTR("No FAT partition found. Card formatted?"));
    Serial.println(line);
    error_idling(2);
  }
  // print the type and size of the first FAT-type volume
  uint32_t volumesize;
  strcpy_P(line,PSTR("Volume type: FAT"));
  Serial.print(line);
  Serial.println(volume.fatType(), DEC);
  delay(2000); // allow some time (2 seconds) to read opening result
  volumesize = volume.blocksPerCluster();    // clusters are collections of blocks
  volumesize *= volume.clusterCount();       // we'll have a lot of clusters
  volumesize *= 512;                         // SDcard blocks are always 512 bytes
  strcpy_P(line,PSTR("Volume size (kbytes): "));
  Serial.print(line);
  volumesize /= 1024;
  Serial.println(volumesize);
  delay(2000); // allow some time (2 seconds) to read volume size result
  offsets[0] = 0;
  proceed = false;
  switch_off = false;
  logging_flag = false;
  hash_written = false;
  data_present = false;
  // initialise SD-card for file access
  if (!SD.begin(chipSelect)) {
    strcpy_P(line,PSTR("Initialization failed!"));
    Serial.println(line);
    error_idling(3);
  }
  // find the next log_(n).txt file which doesn't exist yet
  do {
    filecount++;
    datafile[4] = digit100(filecount);
    datafile[5] = digit10(filecount);
    datafile[6] = digit1(filecount);
  } while (SD.exists(datafile));
  Serial.print("Logging to: ");
  Serial.println(datafile);
  // disable power off switching to prevent damage to SDcard during write cycles
  digitalWrite(Power_hold, HIGH);
  File dataFile = SD.open(datafile, FILE_WRITE);
  // if the file is available, write the identification code as first line
  if (dataFile) {
    digitalWrite(Write2LogLED, HIGH);
    sprintf(buf, "ID: %03d%03d", id_hi, id_lo);
    dataFile.println(buf); // save identification code
    dataFile.close();
    for (byte i=0; i<strlen(buf); i++) calc_hash(buf[i]);
    calc_hash('\r'); calc_hash('\n');
    delay(100);
    digitalWrite(Write2LogLED, LOW);
  }
  // if the file isn't open, pop up an error
  else {
    strcpy_P(line,PSTR("Cannot open the logfile!"));
    error_idling(6);
  }
  // display initial PM-values
  write_data(1, Pm25);
  write_data(4, Pm10);
}

void loop() {
  SDS_Serial.listen();
  while (SDS_Serial.available() == 0) {
    Check_power_switch_status(); // wait for PM-data to arrive
  }
  // retrieve PM-data from the SDS011-module
  ProcessSerialSDSData();
  if (display_mode == 0) {
    // write the PM-data to the optional 7-segment display
    write_data(1, int(Pm25/10+0.5));
    write_data(4, int(Pm10/10+0.5));
  }
  // initiate warning signal when PM10-value is too high
  if (Pm10 > 2000) { digitalWrite(Warning, HIGH); } else { digitalWrite(Warning, LOW); }
  // make a string for assembling the data to the logfile
  String dataString1 = "PM2.5: " + String(Pm25/10) + ", PM10: " + String(Pm10/10);
  Serial.println(dataString1);
  // retrieve temperature and humidity from DHT-sensor
  temp = dht.readTemperature();
  hum = dht.readHumidity();
  if (display_mode == 2) {
    // write temperature and humidity values to the optional 7-segment display
    write_data(1, int(temp+0.5));
    write_data(4, int(hum+0.5));
  }
  // make a string for assembling the data to the logfile
  String dataString2 = "Temp: " + String(temp) + ", Humid: " + String(hum);
  Serial.println(dataString2);
  GPS_Serial.listen();
  while (GPS_Serial.available() == 0) {
    Check_power_switch_status(); // wait for GPS-data to arrive
  }
  // retrieve GPS-data
  while (!proceed) {
    if (GPS_Serial.available() > 0) {
      byteGPS=GPS_Serial.read(); // Read a byte of the serial port of GPS-module
      if (!handle_byte(byteGPS)) {
        reset();
      }
    }
    delay(5);
    Check_power_switch_status();
  }
  for (int i=0; i<offsets[12]+2; i++) {
    Serial.print(buf[i]);
  }
  Serial.println(buf[offsets[12]+2]);
  if (display_mode == 3) {
    if (buf[offsets[2]] == 'V') {
      write_data(1, 88);
      write_data(4, 88);
      delay(500);
      write_data(1, 11);
      write_data(4, 11);
    } else {
      // write the time (hour:min) to the optional 7-segment display
      char timebuf[3];
      timebuf[2] = '\0';
      for (int i=0; i<2; i++) timebuf[i] = buf[offsets[1]+i];
      write_data(4, atoi(timebuf) + hour_inc(offsets[9]));
      for (int i=0; i<2; i++) timebuf[i] = buf[offsets[1]+i+2];
      write_data(1, atoi(timebuf));
    }
  }
  uint16_t v = analogRead(Switch_status);
  uint8_t Vin = int(v/15.5+0.5); // result is Vin in 0.1V steps (voltage divider on board: 5V->2.5V and 10bit ADC for Vref=3.3V -> 1023, hence 2.5V -> 775 and 775/15.5 = 50)
  Serial.println("Vin: " + String(Vin));
  v = analogRead(NO2_pin); // take reading of ADC value from NOX-pin of NO2-sensor
  float Vout = v/207.5; // convert ADC value to voltage (voltage divider on board: 5V->3.2V and 10bit ADC for Vref=3.3V -> 1023, hence 3.2V -> 992 and 992/198.4 = 5,
  Serial.println("Vout: " + String(Vout)); // but because of resistor tolerance of 10% the value 207,5 is taken experimentally by measuring real output voltage on NOX-pin)
  float RlRs = Vout/(Vin/10-Vout); // find load resistance over sensor resistance proportion from Vout, using Vin as supply voltage
  float ppmNO2 = pow(10, 0.9682*log(RlRs)/log(10)-0.8108); // convert RsR0 to ppm concentration NO2 (refer to http://myscope.net/auswertung-der-airpi-gas-sensoren/)
  Serial.println("ppm NO2: " + String(ppmNO2));
  float mgNO2 = (560.5/(273.15+temp))*ppmNO2; // convert ppm concentration to mg NO2/m taking air temperature into account and assuming 1013 mbar atmospheric pressure
  if (display_mode == 1) {
    write_data(1, int(1000*mgNO2+0.5));
    write_data(4, Vin);
  }
  dataString1 += ", NO2: " + String(1000*mgNO2);
  // stop writing to SD card when Vin is below Vmin Volt or GPS-data not available
  if (logging_flag && !switch_off && (Vin < Vmin || buf[offsets[2]] == 'V')) {
    // display error number 4 or 5
    write_data(1, 888);
    write_data(4, 888);
    delay(1000);
    write_data(4, 0);
    if (Vin < Vmin) {
      write_data(1, 4);
      lowpowercount++;
      if (lowpowercount > 5) {
        logging_flag = false;
        write_file_hash();
        delay(1000);
        digitalWrite(Power_hold, LOW);
      }
    }
    if (buf[offsets[2]] == 'V') {
      logging_flag = false;
      write_data(1, 5);
    }
    delay(1000);
  }
  // start writing to SD card when Vin is above Vmin Volt and GPS-data is available
  if (!logging_flag && Vin > Vmin && buf[offsets[2]] == 'A') {
    logging_flag = true;
    delay(1000); // allow some time (1 second)
  }
  Check_power_switch_status();
  if (logging_flag && !hash_written) {
    File dataFile = SD.open(datafile, FILE_WRITE);
    // if the file is available, write to it
    if (dataFile) {
      digitalWrite(Write2LogLED, HIGH);
      dataFile.println(dataString1); // save PM-data & NO2-data
      for (int i=0; i<dataString1.length(); i++) calc_hash(dataString1.charAt(i));
      calc_hash('\r'); calc_hash('\n');
      dataFile.println(dataString2); // save DHT-data
      for (int i=0; i<dataString2.length(); i++) calc_hash(dataString2.charAt(i));
      calc_hash('\r'); calc_hash('\n');
      for (int i=0; i<offsets[12]-1; i++) {
        dataFile.print(buf[i]);      // save GPS-data
        calc_hash(buf[i]);
      }
      dataFile.println(buf[offsets[12]-1]);
      calc_hash(buf[offsets[12]-1]);
      calc_hash('\r'); calc_hash('\n');
      dataFile.close();
      data_present = true;
      delay(100);
      digitalWrite(Write2LogLED, LOW);
    }
    // if the file isn't open, pop up an error
    else {
      strcpy_P(line,PSTR("Cannot open the logfile!"));
      Serial.println(line);
      // display error number 6
      write_data(1, 888);
      write_data(4, 888);
      delay(1000);
      write_data(1, 6);
      write_data(4, 0);
      delay(1000);
    }
  }
  // reset the GPS-data buffer
  for (int i=0; i<200; i++) {
    buf[i] = ' ';
  }
  proceed = false;
}

Credits

Telina

Telina

0 projects • 22 followers

Comments