Maureen Rakotondraibe
Published © GPL3+

IAQ Monitoring using Digital Art w/ RPI & Processing & Cloud

Indoor Air Quality monitoring via soothing digital art to control your air quality aesthetically without over-stressing about it.

IntermediateFull instructions provided6 hours603
IAQ Monitoring using Digital Art w/ RPI & Processing & Cloud

Things used in this project

Hardware components

Raspberry Pi 3 Model B+
Raspberry Pi 3 Model B+
×1
Sensirion SEN54
×1
SparkFun Breadboard to JST-GHR-06V Cable - 6-Pin x 1.25mm Pitch
×1
FREENOVE 5 Inch Touchscreen Monitor for Raspberry Pi
×1

Software apps and online services

Processing
The Processing Foundation Processing
ThingSpeak API
ThingSpeak API
Fusion 360
Autodesk Fusion 360

Story

Read more

Custom parts and enclosures

sen5x_support.step

iaq_monitor_body.step

iaq_monitor_back.step

Schematics

Circuit diagram and Pinouts

Code

SineWave_SEN.pde

Processing
void setup() {
  //size(800, 480);
  fullScreen();
  noCursor();
  init_visual();
  
  //I2C initialization
  I2C_init();
  
  //Sensor initialization
  reset();
  startMeasurement();
  
}

void draw() {
  background(0);
  textDisplay();
  calcWaves();
  renderWaves();
}

//If the touchscreen is pressed, stop the sensor and close the application
void mousePressed() {
  stopMeasurement(); //stop the sensor
  exit();
}

SEN5X_I2C.pde

Processing
import processing.io.*;
I2C i2c;

final byte SEN5X_ADDRESS = 0x69;

void I2C_init(){
  i2c = new I2C(I2C.list()[0]);
}

void startMeasurement(){
  i2c.beginTransmission(SEN5X_ADDRESS);
  i2c.write(0x00);
  i2c.write(0x21);
  i2c.endTransmission();
  delay(50);
}

void stopMeasurement(){
  i2c.beginTransmission(SEN5X_ADDRESS);
  i2c.write(0x01);
  i2c.write(0x04);
  i2c.endTransmission();
  delay(50);
}

void reset(){
  i2c.beginTransmission(SEN5X_ADDRESS);
  i2c.write(0xD3);
  i2c.write(0x04);
  i2c.endTransmission();
  delay(200);
}

float[] readMeasurement(){
  // Send command to read measurement data (0x03C4)
  i2c.beginTransmission(SEN5X_ADDRESS);
  i2c.write(0x03);
  i2c.write(0xC4);
  i2c.endTransmission();
   // Wait 20 ms to allow the sensor to fill the internal buffer
  delay(20); 
  
  i2c.beginTransmission(SEN5X_ADDRESS);
  // Read measurement data of SEN55, after two bytes a CRC follows
  byte[] data = i2c.read(21);

  //bytes are interpreted as signed int in java so val & 0xff is necessary to keep it as an unsigned value
  //concatenating the bytes and discarding the CRC
  int pm1p0 = (data[0] & 0xff) << 8 | (data[1] & 0xff);
  int pm2p5 = (data[3] & 0xff) << 8 | (data[4] & 0xff);
  int pm4p0 = (data[6] & 0xff) << 8 | (data[7] & 0xff);
  int pm10p0 = (data[9] & 0xff) << 8 | (data[10] & 0xff);
  int humidity = (data[12] & 0xff) << 8 | (data[13] & 0xff);
  int temperature = (data[15] & 0xff) << 8 | (data[16] & 0xff) ;
  int voc = (data[18] & 0xff) << 8 | (data[19] & 0xff);
  
  //applying the scale factor to each value
  float[] values = {pm1p0/10, pm2p5/10, pm4p0/10, pm10p0/10, voc/10, temperature/200, humidity/100};
  
  //Optional printing of values for debug purposes
  for(int i = 0; i<7;i++){
    print(values[i]);
    print(" - ");
  }
  print("\n");
  return values;
 
}

HTTP_Thingspeak.pde

Processing
import http.requests.*;
String apiKey = "insert-your-key-here";

void httpPostThingspeak(float VOC, float PM, float humidity, float temperature) {
  PostRequest post = new PostRequest("http://api.thingspeak.com/update");
  post.addData("api_key", apiKey);
  post.addData("field1", str(temperature));
  post.addData("field2", str(humidity));
  post.addData("field3", str(PM));
  post.addData("field4", str(VOC));
  post.send();
  //System.out.println("Reponse Content: " + post.getContent());
  //System.out.println("Reponse Content-Length Header: " + post.getHeader("Content-Length"));
}

Visual_SineWave.pde

Processing
int w;              // Width of entire wave
int xspacing = 16;   // How far apart should each horizontal location be spaced
float theta= 0.0;  // Start angle at 0
float amplitude = 35.0;  // Height of wave

float dx;  // Value for incrementing X, a function of period and xspacing
float[][] yvalues;  // Using an array to store height values for the wave

int periodHUM =500, periodVOC=500, periodPM=500, periodTEMP=500,  periodREF=500; // How many pixels before the wave repeats
float dxVOC,dxHUM,dxPM,dxTEMP,dxREF;  // Value for incrementing X, a function of period and xspacing
float[] yvaluesVOC;  // Using an array to store height values for the wave

float[] wavePosition = new float[5];
final int VOC = 0;
final int PM = 1;
final int HUM = 2;
final int TEMP = 3;
final int REF = 4;

int newPeriodHUM =500, newPeriodVOC=500, newPeriodPM=500, newPeriodTEMP=500; //new period target
int periodChangeHUM, periodChangeVOC, periodChangePM, periodChangeTEMP; // value to add to the previous period
float[] values = {0,0,45,22}; //values from the sensor VOC PM HUM TEMP
float[] changePeriods = new float[4]; //if 1 (TRUE) it means that the period is stable
final int CHANGE = 1; //value added or substracted each frame to get to the new period

//SIMULATION VAUES
int periodChange = -1;
float humI2C = 30;
float tempI2C= 35;
float VOCI2C = 200;
float PMI2C = 25;

int startTime, stopTime, cloudTime = 0; //Timer values


//Fetch data from the sensor every 2 seconds
void fetch_data(){
  if(seconds_check() == 1){
    values = readMeasurement();
    println("/"+periodHUM +"/"+periodVOC+"/"+periodPM+"/"+periodTEMP);
    println("/"+newPeriodHUM +"/"+ newPeriodVOC+"/"+ newPeriodPM+"/"+ newPeriodTEMP);
  }
}
//Timer function to see if 2 seconds has passed
int seconds_check(){
  stopTime =  (int) ((millis() - startTime) / 1000.0);
  if(stopTime>2){  
    startTime = millis();
    cloudTime++;
    if(cloudTime == 5){ //every 10seconds send values to Thingspeak server
      httpPostThingspeak(values[VOC], values[PM], values[HUM], values[TEMP]);
      cloudTime = 0;
    }
    return 1;
  }
  return 0;  
}

//Init function for the curves position
void init_visual(){
  w = width+16;
  yvalues = new float[5][w/xspacing];
  yvaluesVOC = new float[w/xspacing];
  
  wavePosition[VOC] = height/6;
  wavePosition[PM] = height*2/6;
  wavePosition[REF] = height*3/6;
  wavePosition[HUM] = height*4/6;
  wavePosition[TEMP] = height*5/6; 
  startTime = millis();  //Timer start
}

//Init function for the legend displayed on the screen
void textDisplay(){
 fill(255);
 textSize(15);
 text("VOC", 20,  wavePosition[VOC]);
 text("PM2.5", 20,  wavePosition[PM]);
 text("Temperature", 20,  wavePosition[TEMP]);
 text("Humidity", 20,  wavePosition[HUM]);
 text("Reference", 20,  wavePosition[REF]);
}

//Function to compute the curves of the next frame
void calcWaves() {
  theta += 0.02;
  dxHUM = (TWO_PI / periodHUM) * xspacing;
  dxVOC = (TWO_PI / periodVOC) * xspacing;
  dxTEMP = (TWO_PI / periodTEMP) * xspacing;
  dxPM = (TWO_PI / periodPM) * xspacing;
  dxREF = (TWO_PI / periodREF) * xspacing;
  // For every x value, calculate a y value with sine function
  float h = theta,v= theta,t= theta,p= theta,r= theta ;
  for (int i = 0; i < yvalues[0].length; i++) {
    yvalues[HUM][i] = sin(h)*amplitude;
    h+=dxHUM;
    yvalues[VOC][i] = sin(v)*amplitude;
    v+=dxVOC;
    yvalues[TEMP][i] = sin(t)*amplitude;
    t+=dxTEMP;
    yvalues[PM][i] = sin(p)*amplitude;
    p+=dxPM;
    yvalues[REF][i] = sin(r)*amplitude;
    r+=dxREF;
  }
  fetch_data(); //fetch sensor values if 2 seconds has passed
  checkValues(); //check if the latest values are in range and compute the new target period
  checkPeriods(); //check if the period is stable 
  
  //change period for next frame if needed
  periodHUM += periodChangeHUM;
  periodTEMP += periodChangeTEMP;
  periodVOC += periodChangeVOC;
  periodPM += periodChangePM;

}

//Check if the period is stable
void checkPeriods(){
  if(newPeriodHUM == periodHUM){
     periodChangeHUM = 0;
     changePeriods[HUM] = 1;
  }else{
    changePeriods[HUM] = 0;
  }
  
  if(newPeriodTEMP == periodTEMP){
     periodChangeTEMP = 0;
     changePeriods[TEMP] = 1;
  }else{
    changePeriods[TEMP] = 0;
  }
  
  if(newPeriodVOC == periodVOC){
      periodChangeVOC = 0;
     changePeriods[VOC] = 1;
  }else{
    changePeriods[VOC] = 0;
  }
  
  if(newPeriodPM == periodPM){
     periodChangePM = 0;
     changePeriods[PM] = 1;
  }else{
    changePeriods[PM] = 0;
  }
}

//If the period is stable, check if the latest values from the sensor are in range and compute new periods 
void checkValues(){
 if(changePeriods[HUM] == 1){
  if(values[HUM]<40){
      newPeriodHUM = (int)(1000 - values[HUM]*12.5);  //maximum is 40%RH*12.5= 500
   }else if(values[HUM]>50){
      newPeriodHUM = 500 - (int)values[HUM]*3; //minimum is 100%RH*3 = 200
   }else{
      newPeriodHUM = 500;
  }
  if(periodHUM<newPeriodHUM){
        periodChangeHUM = CHANGE;
   }else if(periodHUM>newPeriodHUM){
        periodChangeHUM = -CHANGE;
   }
 }
 if(changePeriods[TEMP] == 1){
   if(values[TEMP]<20){
      newPeriodTEMP = 1000 - (int)values[TEMP]*25;  //20C*25= 500
   }else if(values[TEMP]>25){
      newPeriodTEMP = 500 - (int)values[TEMP]*6; //minimum is 200 period at 50C
   }else{
      newPeriodTEMP = 500;
   }
  if(periodTEMP<newPeriodTEMP){
      periodChangeTEMP = CHANGE;
   }else if(periodTEMP>newPeriodTEMP){
      periodChangeTEMP = -CHANGE;
   }
 }
 
  if(changePeriods[VOC] == 1){
   if(values[VOC]>150){
      newPeriodVOC = (int)(500 - values[VOC]*0.6); //minimum is 200 period at 500 VOC index
   }else{
      newPeriodVOC = 500;
   }
    
   if(periodVOC<newPeriodVOC){
      periodChangeVOC = CHANGE;
    }else if(periodVOC>newPeriodVOC){
      periodChangeVOC = -CHANGE;
    }
 }
 
 if(changePeriods[PM] == 1){
   if(values[PM]>35){
      newPeriodPM = (int)(500 - values[PM]*2); //minimum is 200 period at 200UG/M3 PM2.5
   }else{
      newPeriodPM = 500;
   }
  if(periodPM<newPeriodPM){
      periodChangePM = CHANGE;
    }else if(periodPM>newPeriodPM){
      periodChangePM = -CHANGE;
    }
 }
}

//Display the curves on frame
void renderWaves() {
  for(int curve = 0; curve<wavePosition.length; curve++){
    noStroke();
    colorCurve(curve); //color effect
    // A simple way to draw the wave with an ellipse at each location
    for (int x = 0; x < yvalues[curve].length; x++) {  
      ellipse(x*xspacing, wavePosition[curve]+yvalues[curve][x], 16, 16);
    }
  }
  
}

//Color effect of the curves, slightly changing but staying in the same tone
int humColor = 245;
int vocColor = 235;
int pmColor = 221;
int tempColor = 159;
int humColorChange, vocColorChange, tempColorChange, pmColorChange = -1;

void colorCurve(int curve){
  switch(curve){
    case HUM:
      humColor += humColorChange;
      if(humColor==66){
        humColorChange = 1;
      }else if(humColor == 245){
        humColorChange = -1;
      }
      fill(66, humColor, 245);
      break;
    case VOC:
      vocColor += vocColorChange;
      if(vocColor==140){
        vocColorChange = 1;
      }else if(vocColor == 235){
        vocColorChange = -1;
      }
      fill(vocColor, 120, 235);
      break;
    case PM:
      pmColor += pmColorChange;
      if(pmColor==162){
        pmColorChange = 1;
      }else if(pmColor == 221){
        pmColorChange = -1;
      }
      fill(242, pmColor, 58);
      break;
      
    case TEMP:
      tempColor += tempColorChange;
      if(tempColor==75){
        tempColorChange = 1;
      }else if(tempColor == 159){
        tempColorChange = -1;
      }
      fill(219, tempColor, 75);
      break;
      
    case REF:
      fill(230, 229, 227,60); //4th argument is opacity
      break;
  }
}

Credits

Maureen Rakotondraibe

Maureen Rakotondraibe

1 project • 2 followers
Embedded systems/software engineer / Passionate about interfacing technologies to create new solutions

Comments