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

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

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

  • 88 respects

Components and supplies

Ard nano
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.
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.
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.
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.
10167 01
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.
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.
Fairchild semiconductor lm555cn image
555 Timers
This component has to be soldered on the main interconnection board or has to be plugged into an 8-pin DIP-socket.
7-segment single digit
These single digit display units have to be soldered onto the display board.
74HC4511 BCD to Seven-Segment Decoder / Driver / Latch
These TTL ICs have to be soldered onto the display board.
74LS138 3-to-8 line decoder TTL IC
This TTL IC has to be soldered onto the display board.
Fairchild semiconductor pn2222abu. image
General Purpose Transistor NPN
These transistors have to be soldered onto the main interconnection board.
Fairchild pn2907abu.
General Purpose Transistor PNP
This transistor has to be soldered onto the main interconnection board.
08377 02 l
Resistor 330 ohm
These resistors have to be soldered onto the main interconnection board.
Resistor 560 ohm
This resistor has to be soldered onto the main interconnection board.
Mfr 25frf52 1k sml
Resistor 1k ohm
These resistors have to be soldered onto the main interconnection board.
Mfr 25fbf52 2k21 sml
Resistor 2.21k ohm
This resistor has to be soldered onto the main interconnection board.
Lite on ltl 1che
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.
Dual Pole Dual Throw (DPDT) Switch
This switch has to be connected to the main interconnection board.
Resistor 33k ohm
This resistor has to be soldered onto the main interconnection board.
Bourns 3362p 1 103lf image 160px
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.
Kemet c320c104k5r5ta image
Capacitor 100 nF
This capacitor has to be soldered onto the main interconnection board.
Panasonic eca2am101
Capacitor 100 µF
This capacitor has to be soldered onto the main interconnection board.

Apps and online services

New google maps logo
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

About this project

Project history and goal

Besides food and water, the air we breathe is of utmost importance for our health. However the air we breathe is sometimes heavily polluted by fine dust particles and several toxic gases. At the long term this has a very adverse effect on our health. Some particles and gases like e.g. black carbon and NO2 cannot be detected by smelling their odor, while these substances are very toxic for the respiratory system. Hence, measuring these substances accurately at low cost would be a big relief to create awareness about the quality of the air we breathe.

In 2016 the university of Stuttgart developed a low cost fine dust meter based on the sensor SDS011 of Nova. This sensor has been chosen for its low cost and measurement accuracy. It can measure particulate matter (PM) of sizes smaller than 10 µm (PM10) and 2.5 µm (PM2.5) by using laser diffraction and optical signal processing.

This project has led to the "" network to which more than 30.000 fine dust meter stations are connected in 2018 and new stations are connected every day. This network has grown to be the biggest civilian based network for measuring air quality in Western Europe. Each station consists only of three interconnected modules: the SDS011 fine dust sensor, the DHT22 temperature and humidity sensor and the Lolin NodeMCU 1.0 microboard, which contains the ESP8266 microcontroller, an USB-interface and capability to act as Wifi-station as well as Wifi-AP (access point). At first startup the meter acts as a Wifi-AP to perform an initial configuration, after which the meter transforms in a Wifi-station connected to the home network. Hence, sensor data is sent immediately via each home based Wifi-network over the internet to the central database located in Stuttgart.

However, these meter stations measure the fine dust concentration only at fixed locations. Being a frequent bike commuter among roads loaded by dense traffic, the need arose to measure the air quality whilst on the move by bike. Since traffic exhaust, especially from gasoline vehicles, not only contains fine dust but also a lot of NO2, the aim of this project is to measure fine dust (PM10 and PM2.5) as well as NO2 concentration in the air. Sensor data will not be sent directly into the cloud (being an online database server), but is written to a file on a SDcard and (optionally) can be displayed in real time to a 6-digit 7-segment display.

Technical description

This mobile air quality meter is built around the Arduino Nano microboard. In its simplest and also cheapest version, the Arduino Nano is connected to a SDcard reader via the Serial Peripheral Interface (SPI), the SDS011 fine dust sensor and an Open Smart GPS-module, these latter two sending their data via the classical V24 serial protocol.

The SDcard reader, the GPS-module and the Arduino Nano are plugged into a main PCB which provides all connections between these components in a firm failsafe way, while the SDS011 sensor is connected via a 4-wire flat cable, which is always delivered with this sensor when ordered. The fine dust sensor is attached to the main board with 3 screws.

The main board also houses a DPDT-switch and a simple transistor circuit to take care for a software controlled power-down. This is necessary to prevent power loss during a write cycle to the logfile on the SDcard, which would make the file corrupt or in the worst case would render the SDcard inaccessible. After putting the switch into the off-state, an eventual ongoing write cycle is finished and a timer is started for 2 seconds. If the switch isn't put into the on-state again before timeout, a digital signature is calculated and written to the file before closing it. This signature prevents that the contents of the file can be compromised by editing its data before uploading it to a webserver.

Each time the mobile meter is switched on, a new textfile is created on the SDcard. The name of this file always starts with "log_" followed by a 3-digit sequence number. In the setup of the sketch an algorithm is present to look for the lowest free sequence number to construct a new unique logfile in order to prevent overwriting or appending data to an existing logfile. After a new file is created, the unique IDcode of the mobile meter is written as the first line in the logfile. This IDcode is essential for proper discrimination between all logfiles after these are uploaded to the webserver.

The meter can be enhanced by connecting the DHT22 temperature and humidity sensor and the NO2-sensorboard CJMCU-4541. Measuring humidity is important since extreme low (<10%) and high (>90%) relative humidity has an impact on the fine dust concentration, while temperature is mandatory for calculating the NO2-concentration in µg/m³. When these 3 sensors are present each measurement record is represented by 3 subsequent data lines in the logfile:

  • first line contains the PM10, PM2.5 and NO2 concentrations in µg/m³
  • second line contains the temperature in °C and relative humidity %
  • third line contains the GPRMC sentence containing GPS-coordinates in NMEA-format

Optionally, a 6-digit 7-segment display can be connected to the meter for reading the collected data in real-time while driving. In order to enable this in a secure and comfortable way, the complete set is housed in a waterproof transparant housing which can be opened and closed easily in order to install and remove the SDcard and to replace exhausted batteries. To be able to display all data measured by the three sensors, 4 display modes are provided:

  • mode 0 displays PM10 value (0-999) on the left and PM2.5 (0-999) on the right side
  • mode 1 displays supply voltage (42-51) on the left and NO2 (0-999) on the right side
  • mode 2 displays humidity (0-99) on the left and temperature (0-99) on the right side
  • mode 3 display the time: hours (0-23) on the left and minutes (0-59) on the right side

The time in the last display mode is derived from the GPRMC sentences received from the GPS-module. Through these 4 display modes can be rotated by putting the switch in the off state and back to the on state again before the already mentioned 2 second timer expires. When switching to another display mode, first the mode digit is shown for a while to acknowledge the display of a new set of measured data.

Four AA-type batteries are needed to provide 5V directly to the Arduino Nano board, the GPS-module, the SDcard reader, the fine dust sensor and the NO2-sensor. The DHT22-sensor is connected to the 3.3V output of the Arduino Nano board. Batteries may be rechargeable or not. Battery voltage is constantly sampled and when it drops for the first time below 4.3V, writing to the SDcard is inhibited. After 4 readings of the supply voltage below 4.3V, the active logfile will be closed after writing the digital signature to it and software controlled power down is deactivated so that the device will be switched off immediately when putting the switch in the off position. This voltage monitoring mechanism prevents file corruption or a damaged SD-card due to battery exhaustion.

It's also possible to connect an external warning device to the mobile fine dust meter, like e.g. a flash light or a buzzer to alert the environment that the health hazard threshold of 200 µg/m³ PM10 concentration has been exceeded.

In the image below two photos have been merged showing the internal arrangement of all components on both sides of the plastic housing after it has been opened. This is the prototype of the mobile meter with the display.


Mobile fine dust and NO2 meterArduino
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
    // packet format: AA C0 PM25_Low PM25_High PM10_Low PM10_High 0 0 CRC AB
    mData =;
    delay(2); //wait until packet is received
    if(mData == 0xAA) { //first headerbyte ok
      mPkt[0] =  mData;
      mData =;
      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] =;
           mCheck += mPkt[i+2];
        mPkt[8] =; //get crc
	      mPkt[9] =;
        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

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);
  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);
    write_data(1, error_nbr);
    write_data(4, 0);

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;
  if (counter1 == 200) {
    return false;
  if (byteGPS == ',') {
    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 =, 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]);
        digitalWrite(Write2LogLED, LOW);
    else {

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;
  if (v < 100 && switch_off && abs(millis() - currentTime) > 2000 && !hash_written) {
    logging_flag = false;
    hash_written = true;
    digitalWrite(Power_hold, LOW);
  if (v > 100 && switch_off) {
    switch_off = false;
    ++display_mode %= 4;
    write_data(1, display_mode);
    for (byte i=4; i<7; i++) write_nibble(i, 15);

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
  // start serial connection to GPS-module
  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..."));
  if (!card.init(SPI_HALF_SPEED, chipSelect)) {
    strcpy_P(line,PSTR("Initialization failed."));
  } else {
    strcpy_P(line,PSTR("SD card is OK.  "));
  // print the type of the SDcard
  strcpy_P(line,PSTR("Card type: "));
  switch (card.type()) {
    case SD_CARD_TYPE_SD1:
    case SD_CARD_TYPE_SD2:
  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?"));
  // print the type and size of the first FAT-type volume
  uint32_t volumesize;
  strcpy_P(line,PSTR("Volume type: FAT"));
  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): "));
  volumesize /= 1024;
  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!"));
  // find the next log_(n).txt file which doesn't exist yet
  do {
    datafile[4] = digit100(filecount);
    datafile[5] = digit10(filecount);
    datafile[6] = digit1(filecount);
  } while (SD.exists(datafile));
  Serial.print("Logging to: ");
  // disable power off switching to prevent damage to SDcard during write cycles
  digitalWrite(Power_hold, HIGH);
  File 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
    for (byte i=0; i<strlen(buf); i++) calc_hash(buf[i]);
    calc_hash('\r'); calc_hash('\n');
    digitalWrite(Write2LogLED, LOW);
  // if the file isn't open, pop up an error
  else {
    strcpy_P(line,PSTR("Cannot open the logfile!"));
  // display initial PM-values
  write_data(1, Pm25);
  write_data(4, Pm10);

void loop() {
  while (SDS_Serial.available() == 0) {
    Check_power_switch_status(); // wait for PM-data to arrive
  // retrieve PM-data from the SDS011-module
  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);
  // 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);
  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) {; // Read a byte of the serial port of GPS-module
      if (!handle_byte(byteGPS)) {
  for (int i=0; i<offsets[12]+2; i++) {
  if (display_mode == 3) {
    if (buf[offsets[2]] == 'V') {
      write_data(1, 88);
      write_data(4, 88);
      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
  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);
    write_data(4, 0);
    if (Vin < Vmin) {
      write_data(1, 4);
      if (lowpowercount > 5) {
        logging_flag = false;
        digitalWrite(Power_hold, LOW);
    if (buf[offsets[2]] == 'V') {
      logging_flag = false;
      write_data(1, 5);
  // 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)
  if (logging_flag && !hash_written) {
    File 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('\r'); calc_hash('\n');
      data_present = true;
      digitalWrite(Write2LogLED, LOW);
    // if the file isn't open, pop up an error
    else {
      strcpy_P(line,PSTR("Cannot open the logfile!"));
      // display error number 6
      write_data(1, 888);
      write_data(4, 888);
      write_data(1, 6);
      write_data(4, 0);
  // reset the GPS-data buffer
  for (int i=0; i<200; i++) {
    buf[i] = ' ';
  proceed = false;

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.
Peli 1040 microcase black clear 1 xfd2j42chm
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.
Main interconnection pcb qtpqdwts3n


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.
Main interconnection board 3bgnzepjyi
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.
Display board yxz4thmtqt


Similar projects you might like

Rube Goldberg Weather Station with Internet Data Storage

Project in progress by randtekk

  • 62 respects

Autopilot Drone

Project in progress by suhaskd

  • 195 respects

Soldering Iron Controller for Hakko 907

Project tutorial by Alexander

  • 83 respects

Wireless Water-Tank Level Meter with Alarm

Project showcase by Manusha_Ramanayake

  • 40 respects

Autonomous Indoor Greenhouse - Mature Real Working Project

Project showcase by vinikon

  • 256 respects

Arduino Cloud Sensor Tower

Project tutorial by Arduino_Genuino

  • 122 respects
Add projectSign up / Login