Guntas Singh
Published © GPL3+

MicroENV precision environmental monitor

A compact, versatile, and extremely accurate tool for assessing ALL types of environmental damage

IntermediateFull instructions provided2 hours4,444
MicroENV precision environmental monitor

Things used in this project

Hardware components

Nano 33 BLE Sense
Arduino Nano 33 BLE Sense
×1
SPS30 Particulate Matter Sensor
×1
Seeed Studio Seeedstudio power bank
×1
USB-A to Micro-USB Cable
USB-A to Micro-USB Cable
×5
Seeed Studio SGP30 CO2 and VOC sensor
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Premium Female/Male Extension Jumper Wires, 40 x 6" (150mm)
Premium Female/Male Extension Jumper Wires, 40 x 6" (150mm)
Premium Female/Female Jumper Wires, 40 x 3" (75mm)
Premium Female/Female Jumper Wires, 40 x 3" (75mm)

Story

Read more

Schematics

Complete wiring diagram for MicroENV

USB

Code

MicroENV code

Arduino
Upload to Nano 33 BLE Sense via uncut USB cable. Then, press Reset button on Nano 33 BLE, ensuring that the wiring has been constructed correctly. Once the blue LED on the Nano 33 BLE lights up, go to the nRF scanner app and connect to MicroENV to collect measurement data.
#include <Arduino_HTS221.h>
#include <ArduinoBLE.h>
#include <Arduino_LPS22HB.h>
#include <Arduino_LSM9DS1.h>
#include "sps30.h"
#include <Wire.h>
#include "Adafruit_SGP30.h"
#define INCLUDE_SOFTWARE_SERIAL 0 
#define SP30_COMMS Serial1
#define PERFORMCLEANNOW 1 //Set to 0 if you don't want autocleaning to run on the SPS30 at the start
SPS30 sps30;
Adafruit_SGP30 sgp;
  int counter = 0;

/* return absolute humidity [mg/m^3] with approximation formula
  @param temperature [°C]
  @param humidity [%RH]
*/
uint32_t getAbsoluteHumidity(float temperature, float humidity) {
  // approximation formula from Sensirion SGP30 Driver Integration chapter 3.15
  const float absoluteHumidity = 216.7f * ((humidity / 100.0f) * 6.112f * exp((17.62f * temperature) / (243.12f + temperature)) / (273.15f + temperature)); // [g/m^3]
  const uint32_t absoluteHumidityScaled = static_cast<uint32_t>(1000.0f * absoluteHumidity); // [mg/m^3]
  return absoluteHumidityScaled; }

  float Po = 1013.25; // average pressure at mean sea-level (MSL) in the International Standard Atmosphere (ISA) is 1013.25 hPa
  float P, T;
  float To = 273.15;
  float k = 0.0065;
  void ErrtoMess(char *mess, uint8_t r);
  void Errorloop(char *mess, uint8_t r);
  bool read_all();


  BLEService sensorService("181A");  // Environmental_Sensor-Service
  BLELongCharacteristic sensorLevelChar1("2A6E", BLERead | BLENotify);   // Temperature-Characteristic
  BLELongCharacteristic sensorLevelChar2("2A6F", BLERead | BLENotify);   // Humidity-Characteristic
  BLELongCharacteristic sensorLevelChar3("2A6C", BLERead | BLENotify);    // Elevation-Characteristic
  BLELongCharacteristic sensorLevelChar4("2A6D", BLERead | BLENotify); //Pressure Characteristic
  BLELongCharacteristic sensorLevelChar5("2A75", BLERead | BLENotify); //PM2.5 concentration Characteristic
  BLELongCharacteristic sensorLevelChar6("2A75", BLERead | BLENotify); //eCO2 concentration Characteristic
  BLELongCharacteristic sensorLevelChar7("2A75", BLERead | BLENotify); //TVOC concentration Characteristic





  long previousMillis = 0;

  void setup() {
    Serial.begin(115200);
    Serial1.begin(115200);
    pinMode(LEDB, OUTPUT);
    digitalWrite(LEDB, HIGH);

    // while (!Serial){
    // }

    if (!HTS.begin()) {
      Serial.println("Failed to initialize humidity and/or temperature sensors!");
      while (1);
    }

    if (!BARO.begin()) {
      Serial.println("Failed to initialize pressure sensor!");
      while (1);
    }

    if (!BLE.begin()) {
      Serial.println("BLE initialisation failed!");
      while (1);
    }

    if (! sgp.begin()) {
      Serial.println("Sensor not found :(");
      while (1);
    }
    Serial.print("Found SGP30 serial #");
    Serial.print(sgp.serialnumber[0], HEX);
    Serial.print(sgp.serialnumber[1], HEX);
    Serial.println(sgp.serialnumber[2], HEX);

    // If you have a baseline measurement from before you can assign it to start, to 'self-calibrate'
    //sgp.setIAQBaseline(0x8E68, 0x8F41);  // Will vary for each sensor!


  if (!sps30.begin(SP30_COMMS)) {
    Serial.println("'Couldn't initialise SPS30");

  }

  sps30.EnableDebugging(0);

  // clean now requested
  if (PERFORMCLEANNOW) {
    // clean now
    if (sps30.clean() == true)
      Serial.println(F("fan-cleaning manually started"));
    else
      Serial.println(F("Could NOT manually start fan-cleaning"));
  }



  BLE.setLocalName("MicroENV");
  BLE.addService(sensorService);
  BLE.setAdvertisedService(sensorService);
  sensorService.addCharacteristic(sensorLevelChar1);
  sensorLevelChar1.writeValue(0);

  sensorService.addCharacteristic(sensorLevelChar2);
  sensorLevelChar2.writeValue(0);

  sensorService.addCharacteristic(sensorLevelChar3);
  sensorLevelChar3.writeValue(0);

  sensorService.addCharacteristic(sensorLevelChar4);
  sensorLevelChar4.writeValue(0);

  sensorService.addCharacteristic(sensorLevelChar5);

  sensorService.addCharacteristic(sensorLevelChar6);

  sensorService.addCharacteristic(sensorLevelChar7);

  if (! sps30.begin(SP30_COMMS))
    Errorloop((char *) "could not initialize communication channel.", 0);

  // check for SPS30 connection
  if (! sps30.probe()) Errorloop((char *) "could not probe / connect with SPS30.", 0);
  else  Serial.println(F("Detected SPS30."));

  // reset SPS30 connection
  if (! sps30.reset()) Errorloop((char *) "could not reset.", 0);


  // start measurement
  if (sps30.start()) Serial.println(F("Measurement started"));
  else Errorloop((char *) "Could NOT start measurement", 0);



  // start advertising
  BLE.advertise();
  Serial.print("MicroENV BLE started, waiting for connections...");
 
}

void loop() {
  // waiting for a BLE central device.
  BLEDevice central = BLE.central();
  if (central) {
    Serial.print("Connected to central: ");
    Serial.println(central.address());
    while (central.connected()) {
      long currentMillis = millis();
      if (currentMillis - previousMillis >= 500) {
        previousMillis = currentMillis;
        updateSensorLevel1();
        updateSensorLevel2();
        updateSensorLevel3();
        updateSensorLevel4();
        updateSensorLevel6();
          read_all();
          digitalWrite(LEDB, LOW); // indicate connection
      }
    }
    digitalWrite(LEDB, HIGH); // indicate connection,HIGH);
    Serial.print("Disconnected from central: ");
    Serial.println(central.address());
  }
}

void updateSensorLevel1() {
  float temperature = HTS.readTemperature();
  Serial.print("Temperature = ");
  Serial.print(temperature);
  Serial.println("*C");
  sensorLevelChar1.writeValue(temperature * 100);
}

void updateSensorLevel2() {
  float humidity = HTS.readHumidity();
  Serial.print("Humidity = ");
  Serial.print(humidity);
  Serial.println(" %");
  sensorLevelChar2.writeValue(humidity * 100);
}

void updateSensorLevel3() {
  float pressure = BARO.readPressure();
  P = pressure * 10; // in hectoPascals. // [1hPa = 10kPa]
  float fP = (Po / P);     // pressure-factor.
  float x = (1 / 5.257);   // power-constant.
  double z = pow(fP, x);   // exponential pressure-grad.
  float height = (((z - 1) * (T + To)) / k);

  Serial.print("Altitude = ");
  Serial.print(height);
  Serial.println(" metres");
  sensorLevelChar3.writeValue(height * 100);
}

void updateSensorLevel4() {
  float pressure = BARO.readPressure();
  Serial.print("Pressure = ");
  Serial.print(pressure);
  Serial.println("kPa");
  sensorLevelChar4.writeValue(pressure * 1000);
}

void updateSensorLevel6() {
  if (! sgp.IAQmeasure()) {
    Serial.println("Measurement failed");
    sensorLevelChar6.writeValue(0);
    sensorLevelChar7.writeValue(0);
    return;
  }
  Serial.print("TVOC "); Serial.print(sgp.TVOC); Serial.print(" ppb\t");
  Serial.print("eCO2 "); Serial.print(sgp.eCO2); Serial.println(" ppm");
  sensorLevelChar6.writeValue(sgp.eCO2);
  sensorLevelChar7.writeValue(sgp.TVOC);

  if (! sgp.IAQmeasureRaw()) {
    Serial.println("Raw Measurement failed");

    return;
  }
  Serial.print("Raw H2 "); Serial.print(sgp.rawH2); Serial.print(" \t");
  Serial.print("Raw Ethanol "); Serial.print(sgp.rawEthanol); Serial.println("");

  delay(1000);

  counter++;
  if (counter == 30) {
    counter = 0;

    uint16_t TVOC_base, eCO2_base;
    if (! sgp.getIAQBaseline(&eCO2_base, &TVOC_base)) {
      Serial.println("Failed to get baseline readings");
      return;
    }
  }
}

  bool read_all()
  {
    static bool header = true;
    uint8_t ret, error_cnt = 0;
    struct sps_values val;

    // loop to get data
    do {

      ret = sps30.GetValues(&val);

      // data might not have been ready
      if (ret == SPS30_ERR_DATALENGTH) {

        if (error_cnt++ > 3) {
          ErrtoMess((char *) "Error during reading values: ", ret);
          return (false);
        }
        delay(1000);
      }

      // if other error
      else if (ret != SPS30_ERR_OK) {
        ErrtoMess((char *) "Error during reading values: ", ret);
        return (false);
      }

    } while (ret != SPS30_ERR_OK);

    // only print header first time
    if (header) {
      Serial.println(F("-------------Mass -----------    ------------- Number --------------   -Average-"));
      Serial.println(F("     Concentration [μg/m3]             Concentration [#/cm3]             [μm]"));
      Serial.println(F("P1.0\tP2.5\tP4.0\tP10\tP0.5\tP1.0\tP2.5\tP4.0\tP10\tPartSize\n"));
      header = true;
    }

    Serial.print(val.MassPM1);
    Serial.print(F("\t"));
    Serial.print(val.MassPM2);
    Serial.print(F("\t"));
    Serial.print(val.MassPM4);
    Serial.print(F("\t"));
    Serial.print(val.MassPM10);
    Serial.print(F("\t"));
    Serial.print(val.NumPM0);
    Serial.print(F("\t"));
    Serial.print(val.NumPM1);
    Serial.print(F("\t"));
    Serial.print(val.NumPM2);
    Serial.print(F("\t"));
    Serial.print(val.NumPM4);
    Serial.print(F("\t"));
    Serial.print(val.NumPM10);
    Serial.print(F("\t"));
    Serial.print(val.PartSize);
    Serial.print(F("\n"));
    sensorLevelChar5.writeValue(val.NumPM2);
    return (true);
  }

  void Errorloop(char *mess, uint8_t r)
  {
    if (r) ErrtoMess(mess, r);
    else Serial.println(mess);
    Serial.println(F("Program on hold"));
    for (;;) delay(100000);
  }

  /**
      @brief : display error message
      @param mess : message to display
      @param r : error code

  */
  void ErrtoMess(char *mess, uint8_t r)
  {
    char buf[80];

    

    Serial.print(mess);

    sps30.GetErrDescription(r, buf, 80);
    Serial.println(buf);
  }

Credits

Guntas Singh

Guntas Singh

2 projects • 16 followers
Amateur physicist and IGCSE student. Studying Arduino and Verilog (once I receive my MKR Vidor 4000)

Comments