SGP30 on AirGradient board

Finally success…

TVOC is reporting data now. Still have to keep the stupid serial monitor on to get data from the CO sensor though…

# HELP TVOC
# TYPE TVOC gauge
tvoc{id="testboard",mac="A4:E5:7C:B3:XX:XX"}2
# HELP eCO2
# TYPE eCO2 gauge
eCO2{id="testboard",mac="A4:E5:7C:B3:XX:XX"}400
# HELP H2
# TYPE H2 gauge
H2{id="testboard",mac="A4:E5:7C:B3:XX:XX"}0
# HELP Ethanol
# TYPE Ethanol gauge
Ethanol{id="testboard",mac="A4:E5:7C:B3:XX:XX"}0
# HELP pm02 Particulate Matter PM2.5 value
# TYPE pm02 gauge
pm02{id="testboard",mac="A4:E5:7C:B3:XX:XX"}3
# HELP rco2 CO2 value, in ppm
# TYPE rco2 gauge
rco2{id="testboard",mac="A4:E5:7C:B3:XX:XX"}615
# HELP atmp Temperature, in degrees Fahrenheit
# TYPE atmp gauge
atmp{id="testboard",mac="A4:E5:7C:B3:XX:XX"}75.92
# HELP rhum Relative humidity, in percent
# TYPE rhum gauge
rhum{id="testboard",mac="A4:E5:7C:B3:XX:XX"}41

The sketch:

/**
 * This sketch connects an AirGradient DIY sensor to a WiFi network, and runs a
 * tiny HTTP server to serve air quality metrics to Prometheus.
 */

#include <AirGradient.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <WiFiClient.h>
#include "Adafruit_SGP30.h"
#include <Wire.h>
#include "SSD1306Wire.h"

AirGradient ag = AirGradient();
Adafruit_SGP30 SGP;
// Config ----------------------------------------------------------------------

// Optional.
const char* deviceId = "testboard";

// Hardware options for AirGradient DIY sensor.
const bool hasPM = true;
const bool hasCO2 = true;
const bool hasSHT = true;
const bool hasTVOC = true;

// WiFi and IP connection info.
const char* ssid = "XXXXXXX";
const char* password = "XXXXXXX";
const int port = 9925;

// Uncomment the line below to configure a static IP address.
// #define staticip
#ifdef staticip
IPAddress static_ip(192, 168, 0, 0);
IPAddress gateway(192, 168, 0, 0);
IPAddress subnet(255, 255, 255, 0);
#endif

// The frequency of measurement updates.
const int updateFrequency = 5000;

// For housekeeping.
long lastUpdate;
int counter = 0;

// Config End ------------------------------------------------------------------

SSD1306Wire display(0x3c, SDA, SCL);
ESP8266WebServer server(port);

void setup() {
  Serial.begin(115200);

  // Init Display.
  display.init();
  display.flipScreenVertically();
  showTextRectangle("Init", String(ESP.getChipId(),HEX),true);

  // Enable enabled sensors.
  if (hasTVOC) SGP.begin();
  if (hasPM) ag.PMS_Init();
  if (hasCO2) ag.CO2_Init();
  if (hasSHT) ag.TMP_RH_Init(0x44);

  // Set static IP address if configured.
  #ifdef staticip
  WiFi.config(static_ip,gateway,subnet);
  #endif

  // Set WiFi mode to client (without this it may try to act as an AP).
  WiFi.mode(WIFI_STA);
  
  // Configure Hostname
  if ((deviceId != NULL) && (deviceId[0] == '\0')) {
    Serial.printf("No Device ID is Defined, Defaulting to board defaults");
  }
  else {
    wifi_station_set_hostname(deviceId);
    WiFi.setHostname(deviceId);
  }
  
  // Setup and wait for WiFi.
  WiFi.begin(ssid, password);
  Serial.println("");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    showTextRectangle("Trying to", "connect...", true);
    Serial.print(".");
  }

  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  Serial.print("MAC address: ");
  Serial.println(WiFi.macAddress());
  Serial.print("Hostname: ");
  Serial.println(WiFi.hostname());
  server.on("/", HandleRoot);
  server.on("/metrics", HandleRoot);
  server.onNotFound(HandleNotFound);

  server.begin();
  Serial.println("HTTP server started at ip " + WiFi.localIP().toString() + ":" + String(port));
  showTextRectangle("Listening To", WiFi.localIP().toString() + ":" + String(port),true);
  if (hasTVOC) {
  Serial.print("Found SGP30 serial #");
  Serial.print(SGP.serialnumber[0], HEX);
  Serial.print(SGP.serialnumber[1], HEX);
  Serial.println(SGP.serialnumber[2], HEX);
  }
}

void loop() {

  long t = millis();

  server.handleClient();
  updateScreen(t);
  SGP.IAQmeasure();

}


String GenerateMetrics() {
  String message = "";
  String idString = "{id=\"" + String(deviceId) + "\",mac=\"" + WiFi.macAddress().c_str() + "\"}";
  
  if (hasTVOC) {
    int stat = SGP.TVOC;

    message += "# HELP TVOC\n";
    message += "# TYPE TVOC gauge\n";
    message += "tvoc";
    message += idString;
    message += String(stat);
    message += "\n";
  }
  
  if (hasTVOC) {
    int stat = SGP.eCO2;

    message += "# HELP eCO2\n";
    message += "# TYPE eCO2 gauge\n";
    message += "eCO2";
    message += idString;
    message += String(stat);
    message += "\n";
  }
  
  if (hasTVOC) {
    int stat = SGP.rawH2;

    message += "# HELP H2\n";
    message += "# TYPE H2 gauge\n";
    message += "H2";
    message += idString;
    message += String(stat);
    message += "\n";
  }
  
  if (hasTVOC) {
    int stat = SGP.rawEthanol;

    message += "# HELP Ethanol\n";
    message += "# TYPE Ethanol gauge\n";
    message += "Ethanol";
    message += idString;
    message += String(stat);
    message += "\n";
  } 

  if (hasPM) {
    int stat = ag.getPM2_Raw();

    message += "# HELP pm02 Particulate Matter PM2.5 value\n";
    message += "# TYPE pm02 gauge\n";
    message += "pm02";
    message += idString;
    message += String(stat);
    message += "\n";
  }

  if (hasCO2) {
    int stat = ag.getCO2_Raw();

    message += "# HELP rco2 CO2 value, in ppm\n";
    message += "# TYPE rco2 gauge\n";
    message += "rco2";
    message += idString;
    message += String(stat);
    message += "\n";
  }

  if (hasSHT) {
    TMP_RH stat = ag.periodicFetchData();

    message += "# HELP atmp Temperature, in degrees Fahrenheit\n";
    message += "# TYPE atmp gauge\n";
    message += "atmp";
    message += idString;
    message += String((stat.t * 1.8) + 32);
    message += "\n";

    message += "# HELP rhum Relative humidity, in percent\n";
    message += "# TYPE rhum gauge\n";
    message += "rhum";
    message += idString;
    message += String(stat.rh);
    message += "\n";
  }

  return message;
}

void HandleRoot() {
  server.send(200, "text/plain", GenerateMetrics() );
}

void HandleNotFound() {
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  for (uint i = 0; i < server.args(); i++) {
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }
  server.send(404, "text/html", message);
}

// DISPLAY
void showTextRectangle(String ln1, String ln2, boolean small) {
  display.clear();
  display.setTextAlignment(TEXT_ALIGN_LEFT);
  if (small) {
    display.setFont(ArialMT_Plain_16);
  } else {
    display.setFont(ArialMT_Plain_24);
  }
  display.drawString(32, 16, ln1);
  display.drawString(32, 36, ln2);
  display.display();
}

void updateScreen(long now) {
  
  

  if ((now - lastUpdate) > updateFrequency) {
    // Take a measurement at a fixed interval.
    switch (counter) {
      case 0:
        if (hasPM) {
          int stat = ag.getPM2_Raw();
          showTextRectangle("PM2",String(stat),false);
        }
        break;
      case 1:
        if (hasCO2) {
          int stat = ag.getCO2_Raw();
          showTextRectangle("CO2", String(stat), false);
        }
        break;
      case 2:
        if (hasSHT) {
          TMP_RH stat = ag.periodicFetchData();
          showTextRectangle("TMP", String((stat.t * 1.8) + 32, 1) + "F", false);
        }
        break;
      case 3:
        if (hasSHT) {
          TMP_RH stat = ag.periodicFetchData();
          showTextRectangle("HUM", String(stat.rh) + "%", false);
        }
        break;
      case 4:
        if (hasTVOC) {
          int stat = SGP.TVOC;
          showTextRectangle("TVOC", String(stat), false);
          //Serial.print("TVOC "); Serial.print(SGP.TVOC); Serial.println(" ppb\t");
        }
        break;
       case 5:
        if (hasTVOC) {
          int stat = SGP.eCO2;
          showTextRectangle("eCO2", String(stat), false);
          //Serial.print("eCO2 "); Serial.print(SGP.eCO2); Serial.println(" ppm");
        }
        break;
       case 6:
        if (hasTVOC) {
          int stat = SGP.rawH2;
          showTextRectangle("H2", String(stat), false);
          //Serial.print("rawH2 "); Serial.print(SGP.rawH2); Serial.println(" ppm");
        }
        break;
       case 7:
        if (hasTVOC) {
          int stat = SGP.rawEthanol;
          showTextRectangle("Ethanol", String(stat), false);
          //Serial.print("Ethanol "); Serial.print(SGP.rawEthanol); Serial.println(" ppm");
        }
        break;
    }
    counter++;
    if (counter > 7) counter = 0;
    lastUpdate = millis();
  }
}

I have a strange situation with the below SGP30 sensor from Amazon using the DIY v2 board.
https://www.amazon.com/Quality-Formaldehyde-Detector-Environmental-Monitoring/dp/B085NW2JLJ/ref=sr_1_4?keywords=sgp30+sensor&qid=1651594843&sprefix=SGP30%2Caps%2C127&sr=8-4

The SGP30 sensor produces a 0 reading for TVOC when plugged into the 5v port or the 3v port with the SHT3X sensor connected, but works when the SHT3X is disconnected and the SGP30 is connected to the 3v port.

Going to give the triangular sensor a try as soon as it arrives.

I would suggest to check if a higher rated power plug solves it

Good idea - I tried with my 5v 3.5A raspberry pi power supply but doesnt appear to have made any difference - at the moment I can either use the temperature sensor or the voc sesnor but not both. Going to try removing the display and wiring the tvoc directly to the wemos board.

For folks having trouble with SGP30 communication errors, has anyone tried lowering the I2C data rate? I think by default the ESP8266 library sets it to 700 khz, which may be too fast for I2C fast-rate devices (400 khz max). Datasheets are hard to find, but at least some SGP30 chips are listed as I2C fast-rate.

The power supply nor data rate is the issue.

It’s a noise issue on the board. I had a redesigned board made by jlcpcb that eliminates the issue.

Do you still have any spare copies of your board?

I had an SGP30 laying around so I added it to the board while removing the two pull up resistors on the SHT30. Hack seems to work somewhat. I say somewhat because now I notice that every so often the temperature and humidity will show 0 on the OLED for a while then normal values will show again. It didn’t do this before and it also now does it even without the SGP30 being connected to the board.

Make sure you have very clean solder points. I had some of these issues and reheating the solder points fixed it.

They are clean. I double checked them. The SHT30 was working just fine before I removed the resistors. I will try heating them but I ordered a new SHT sensor just in case.

The Temp and humidity graph look very choppy now. I guess this is is due to the 0s and the backend averaging values:


I did as suggested twice but that didn’t change a thing. Then I wondered if the issue could not be coming from the port from which I was connecting. Bingo. When cable is connected directly to the microcontroller I don’t see these sudden and quick drops to 0s. This only happens when plugging the cable to the usb port on the board. There is also another indication that this is the problem is that when plugging the cable to the microcontroller, just after boot when all the data appears on the OLED, all values populate and remain. On the contrary when plugging to the port on the board data will appear temporarily but then drop to 0s and immediately back to the measured value. I can replicate this each time.

So removing the pull up resistors is not really a solution as it create other issues.

Can you please make high res pictures of the board from both sides and post them here. I would like to have a look if I notice something. Thanks!

Is this ok?



Scrap that. It seems it is now doing it from the microcontroller port as well :man_shrugging:. Strangely when I tested it several times earlier it was more stable. Tomorrow I’ll try adding back the pull up resistors see if that fixes the issue.

With pullup on both sensors one pulls to 3.3v and the other to 5v. This can cause communication errors. And maybe cause your 0 value readings and possibly also harm the SGP30 sensor because it’s rated for lower voltage. Not sure if yours protected for that. I’ve had 2 SHT30 died on me tinkering with this.

Well it died on me. I tried adding back the resistors but they are so small that it was pretty tricky to put them back. I think I ended up burning one of the resistors. So removed them again. Now the sensor is persistently showing 0 so it’s pretty dead. Luckily I had one spare sensor. Anyhow for now I have removed the SGP30 sensor awaiting that Airgradient releases their version.

I also managed to fry the Wemos microcontroler while I was plugging it to the board. I inadvertently offset the pins by 1 row as I was not paying attention. Arduino IDE is not recognizing it anymore so I think it’s dead. Yesterday was not my day!!

One of the next revisions will make sure voltages on the i2c bus cannot be mixed. However having said this, we could not detect any malfunctioning even forcing that issue.
We also work and test a custom made SGP4x module for our board that should run out of the box.

I have a bunch of the original AirGradient boards, and I’m looking to add the SPG30 (aliexpress one) to the board. If I understand the I2C devices correctly should I just be able to stack the SGP30 on top of the SHT30?

The header I’m using would work and I could just solder it right on top. Just wondering if this will cause issues or if it will even work given the issues others have had. Thanks!

We do not recommend to use the SGP30 modules eg from AliExpress as people had problems removing the pull up resistors and often the builds became unstable.

The SGP41 module that we offer in our shop does not have pull ups and works stable.

I found this SGP41 sensor from aliexpress with the correct pinout.


Are the 103 resistors the ones to be removed?
Also in most threads I read it seems to be recommended to remove the resistors from the SHT rather than the SGP. Why is that?