In preparation for a project to monitor the fumes (initially Volatile Organic Compounds) levels around the 3D Printers and Laser Cutters in a school makerspace I purchased a Grove -VOC and eCO2 Gas Sensor (SGP30) for evaluation.

Seeeduino Nano devices have a single on-board I2C socket which meant I didn’t need a Grove Shield for Arduino Nano which reduced the size and cost of the sensor node.
I downloaded the sample code from the Seeedstudio wiki and modified my Easy Sensors Arduino Nano Radio Shield RFM69/95 Payload Addressing client to use the sensor.
My first attempt failed with an issues accessing an Analog port to read the serial number from the Microchip ATSHA204 security chip. After looking at the Seeed SGP30 library source code (based on Sensiron samples) I think the my Nano device was running out of memory. I then searched for other Arduino compatible SGP30 libraries and rebuilt he application with the one from Sparkfun,
/* Copyright ® 2019 August devMobile Software, All Rights Reserved THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. You can do what you want with this code, acknowledgment would be nice. http://www.devmobile.co.nz Seeedstudio Grove - VOC and eCO2 Gas Sensor (SGP30) https://www.seeedstudio.com/Grove-VOC-and-eCO2-Gas-Sensor-SGP30-p-3071.html Seeeduino Nano https://www.seeedstudio.com/Seeeduino-Nano-p-4111.html Polycarbonate enclosure approx 3.5" x 4.5" 2 x Cable glands 1 x Grommet to seal SMA antenna connector 3M command adhesive strips to hold battery & device in place */ #include <stdlib.h> #include "SparkFun_SGP30_Arduino_Library.h" #include <LoRa.h> #include <sha204_library.h> //#define DEBUG //#define DEBUG_TELEMETRY //#define DEBUG_LORA #define DEBUG_VOC_AND_CO2 #define UNITS_VOC "ppb" #define UNITS_CO2 "ppm" // LoRa field gateway configuration (these settings must match your field gateway) const byte DeviceAddressMaximumLength = 15 ; const char FieldGatewayAddress[] = {"LoRaIoT1"}; const float FieldGatewayFrequency = 915000000.0; const byte FieldGatewaySyncWord = 0x12 ; // Payload configuration const int ChipSelectPin = 10; const int ResetPin = 9; const int InterruptPin = 2; // LoRa radio payload configuration const byte SensorIdValueSeperator = ' ' ; const byte SensorReadingSeperator = ',' ; const unsigned long SensorUploadDelay = 60000; // ATSHA204 secure authentication, validation with crypto and hashing (currently only using for unique serial number) const byte Atsha204Port = A3; atsha204Class sha204(Atsha204Port); const byte DeviceSerialNumberLength = 9 ; byte deviceSerialNumber[DeviceSerialNumberLength] = {""}; SGP30 mySensor; //create an object of the SGP30 class const byte PayloadSizeMaximum = 64 ; byte payload[PayloadSizeMaximum]; byte payloadLength = 0 ; void setup() { Serial.begin(9600); #ifdef DEBUG while (!Serial); #endif Serial.println("Setup called"); Serial.print("Field gateway:"); Serial.print(FieldGatewayAddress ) ; Serial.print(" Frequency:"); Serial.print( FieldGatewayFrequency,0 ) ; Serial.print("MHz SyncWord:"); Serial.print( FieldGatewaySyncWord ) ; Serial.println(); // Retrieve the serial number then display it nicely if(sha204.getSerialNumber(deviceSerialNumber)) { Serial.println("sha204.getSerialNumber failed"); while (true); // Drop into endless loop requiring restart } Serial.print("SNo:"); DisplayHex( deviceSerialNumber, DeviceSerialNumberLength); Serial.println(); Serial.println("LoRa setup start"); // override the default chip select and reset pins LoRa.setPins(ChipSelectPin, ResetPin, InterruptPin); if (!LoRa.begin(FieldGatewayFrequency)) { Serial.println("LoRa begin failed"); while (true); // Drop into endless loop requiring restart } // Need to do this so field gateway pays attention to messsages from this device LoRa.enableCrc(); LoRa.setSyncWord(FieldGatewaySyncWord); #ifdef DEBUG_LORA LoRa.dumpRegisters(Serial); #endif Serial.println("LoRa Setup done."); // Configure the DF Robot SHT20, temperature & humidity sensor Serial.println("SGP30 setup start"); Wire.begin(); if(mySensor.begin() == false) { Serial.println("SQP-30 initialisation failed"); while (true); // Drop into endless loop requiring restart } mySensor.initAirQuality(); delay(1000); Serial.println("SGP30 setup done"); PayloadHeader((byte *)FieldGatewayAddress,strlen(FieldGatewayAddress), deviceSerialNumber, DeviceSerialNumberLength); Serial.println("Setup done"); Serial.println(); } void loop() { unsigned long currentMilliseconds = millis(); Serial.println("Loop called"); mySensor.measureAirQuality(); PayloadReset(); PayloadAdd( "v", mySensor.TVOC, false); PayloadAdd( "c", mySensor.CO2, false); #ifdef DEBUG_VOC_AND_CO2 Serial.print("VoC:"); Serial.print( mySensor.TVOC ) ; Serial.print( UNITS_VOC ) ; Serial.print(" Co2:"); Serial.print( mySensor.CO2 ) ; Serial.println( UNITS_CO2 ) ; #endif #ifdef DEBUG_TELEMETRY Serial.println(); Serial.print("RFM9X/SX127X Payload length:"); Serial.print(payloadLength); Serial.println(" bytes"); #endif LoRa.beginPacket(); LoRa.write(payload, payloadLength); LoRa.endPacket(); Serial.println("Loop done"); Serial.println(); delay(SensorUploadDelay - (millis() - currentMilliseconds )); } void PayloadHeader( const byte *to, byte toAddressLength, const byte *from, byte fromAddressLength) { byte addressesLength = toAddressLength + fromAddressLength ; payloadLength = 0 ; // prepare the payload header with "To" Address length (top nibble) and "From" address length (bottom nibble) payload[payloadLength] = (toAddressLength << 4) | fromAddressLength ; payloadLength += 1; // Copy the "To" address into payload memcpy(&payload[payloadLength], to, toAddressLength); payloadLength += toAddressLength ; // Copy the "From" into payload memcpy(&payload[payloadLength], from, fromAddressLength); payloadLength += fromAddressLength ; } void PayloadAdd( const char *sensorId, float value, byte decimalPlaces, bool last) { byte sensorIdLength = strlen( sensorId ) ; memcpy( &payload[payloadLength], sensorId, sensorIdLength) ; payloadLength += sensorIdLength ; payload[ payloadLength] = SensorIdValueSeperator; payloadLength += 1 ; payloadLength += strlen( dtostrf(value, -1, decimalPlaces, (char *)&payload[payloadLength])); if (!last) { payload[ payloadLength] = SensorReadingSeperator; payloadLength += 1 ; } #ifdef DEBUG_TELEMETRY Serial.print("PayloadAdd float-payloadLength:"); Serial.print( payloadLength); Serial.println( ); #endif } void PayloadAdd( char *sensorId, int value, bool last ) { byte sensorIdLength = strlen(sensorId) ; memcpy(&payload[payloadLength], sensorId, sensorIdLength) ; payloadLength += sensorIdLength ; payload[ payloadLength] = SensorIdValueSeperator; payloadLength += 1 ; payloadLength += strlen(itoa( value,(char *)&payload[payloadLength],10)); if (!last) { payload[ payloadLength] = SensorReadingSeperator; payloadLength += 1 ; } #ifdef DEBUG_TELEMETRY Serial.print("PayloadAdd int-payloadLength:" ); Serial.print(payloadLength); Serial.println( ); #endif } void PayloadAdd( char *sensorId, unsigned int value, bool last ) { byte sensorIdLength = strlen(sensorId) ; memcpy(&payload[payloadLength], sensorId, sensorIdLength) ; payloadLength += sensorIdLength ; payload[ payloadLength] = SensorIdValueSeperator; payloadLength += 1 ; payloadLength += strlen(utoa( value,(char *)&payload[payloadLength],10)); if (!last) { payload[ payloadLength] = SensorReadingSeperator; payloadLength += 1 ; } #ifdef DEBUG_TELEMETRY Serial.print("PayloadAdd uint-payloadLength:"); Serial.print(payloadLength); Serial.println( ); #endif } void PayloadReset() { byte fromAddressLength = payload[0] & 0xf ; byte toAddressLength = payload[0] >> 4 ; payloadLength = toAddressLength + fromAddressLength + 1; } void DisplayHex( byte *byteArray, byte length) { for (int i = 0; i < length ; i++) { // Add a leading zero if ( byteArray[i] < 16) { Serial.print("0"); } Serial.print(byteArray[i], HEX); if ( i < (length-1)) // Don't put a - after last digit { Serial.print("-"); } } }
The code is available on GitHub.
11:32:52.947 -> Setup called 11:32:52.947 -> Field gateway:LoRaIoT1 Frequency:915000000MHz SyncWord:18 11:32:53.085 -> SNo:01-23-21-61-D6-D1-F5-86-EE 11:32:53.118 -> LoRa setup start 11:32:53.118 -> LoRa Setup done. 11:32:53.153 -> SGP30 setup start 11:32:54.083 -> SGP30 setup done 11:32:54.117 -> Setup done 11:32:54.117 -> 11:32:54.117 -> Loop called 11:32:54.152 -> VoC:0ppb Co2:400ppm 11:32:54.187 -> Loop done 11:32:54.187 -> 11:33:54.092 -> Loop called 11:33:54.127 -> VoC:0ppb Co2:400ppm 11:33:54.195 -> Loop done 11:33:54.195 -> 11:34:54.098 -> Loop called 11:34:54.133 -> VoC:17ppb Co2:425ppm 11:34:54.201 -> Loop done 11:34:54.201 -> 11:35:54.109 -> Loop called 11:35:54.142 -> VoC:11ppb Co2:421ppm 11:35:54.210 -> Loop done 11:35:54.210 -> 11:36:54.109 -> Loop called 11:36:54.143 -> VoC:3ppb Co2:409ppm 11:36:54.212 -> Loop done 11:36:54.212 -> 11:37:54.135 -> Loop called 11:37:54.135 -> VoC:12ppb Co2:400ppm 11:37:54.204 -> Loop done 11:37:54.204 -> 11:38:54.126 -> Loop called 11:38:54.161 -> VoC:11ppb Co2:439ppm 11:38:54.231 -> Loop done
To configure the device in Azure IoT Central (similar process for Adafruit.IO, working on support for losant, ubidots and MyDevices) I copied the SNo: from the Arduino development tool logging window and appended c for the CO2 parts per million (ppm), v for VOC parts per billion (ppb) unique serial number from the ATSHA204A chip. (N.B. pay attention to the case of the field names they are case sensitive)

Overall the performance of the VoC sensor data is looking pretty positive, the eCO2 readings need some further investigation as they track the VOC levels. The large spike in the graph below is me putting an open vivid marker on my desk near the sensor.

Bill of materials (prices as at August 2019)