In preparation for a project to monitor the particulates levels around the 3D Printers and Laser Cutters in a school makerspace I purchased a Grove -Laser PM2.5 Sensor (HM3301) for evaluation.

The Seeeduino Nano devices I’m testing 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.
To test my setup I installed the Seeed PM2.5 Sensor HM3301 Software Library and downloaded the demo application to my device.
I started with my Easy Sensors Arduino Nano Radio Shield RFM69/95 Payload Addressing client and modified it to use the HM3301 sensor.
After looking at the demo application I stripped out the checksum code and threw the rest away. In my test harness I have extracted only the PM1.0/PM2.5/PM10.0 (concentration CF=1, Standard particulate) in μg/ m3 values from the sensor response payload.
/* 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 */ #include <stdlib.h> #include <LoRa.h> #include <sha204_library.h> #include "Seeed_HM330X.h" //#define DEBUG //#define DEBUG_TELEMETRY //#define DEBUG_LORA const byte SensorPayloadLength = 28 ; const byte SensorPayloadBufferSize = 29 ; const byte SensorPayloadPM1_0Position = 4; const byte SensorPayloadPM2_5Position = 6; const byte SensorPayloadPM10_0Position = 8; HM330X sensor; byte SensorPayload[SensorPayloadBufferSize]; // 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] = {""}; 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 Seeedstudio CO2, temperature & humidity sensor Serial.println("HM3301 setup start"); if(sensor.init()) { Serial.println("HM3301 init failed"); while (true); // Drop into endless loop requiring restart } delay(100); Serial.println("HM3301 setup done"); PayloadHeader((byte *)FieldGatewayAddress,strlen(FieldGatewayAddress), deviceSerialNumber, DeviceSerialNumberLength); Serial.println("Setup done"); Serial.println(); } void loop() { unsigned long currentMilliseconds = millis(); byte sum=0; short pm1_0 ; short pm2_5 ; short pm10_0 ; Serial.println("Loop called"); if(sensor.read_sensor_value(SensorPayload,SensorPayloadBufferSize) == NO_ERROR) { // Calculate then validate the payload "checksum" for(int i=0;i<SensorPayloadLength;i++) { sum+=SensorPayload[i]; } if(sum!=SensorPayload[SensorPayloadLength]) { Serial.println("Invalid checksum"); return; } PayloadReset(); pm1_0 = (u16)SensorPayload[SensorPayloadPM1_0Position]<<8|SensorPayload[SensorPayloadPM1_0Position+1]; Serial.print("PM1.5: "); Serial.print(pm1_0); Serial.println("ug/m3 ") ; PayloadAdd( "P10", pm1_0, false); pm2_5 = (u16)SensorPayload[SensorPayloadPM2_5Position]<<8|SensorPayload[SensorPayloadPM2_5Position+1]; Serial.print("PM2.5: "); Serial.print(pm2_5); Serial.println("ug/m3 ") ; PayloadAdd( "P25", pm2_5, 1, false); pm10_0 = (u16)SensorPayload[SensorPayloadPM10_0Position]<<8|SensorPayload[SensorPayloadPM10_0Position+1]; Serial.print("PM10.0: "); Serial.print(pm10_0); Serial.println("ug/m3 "); PayloadAdd( "P100", pm10_0, 0, true) ; #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.
20:45:38.021 -> Setup called 20:45:38.054 -> Field gateway:LoRaIoT1 Frequency:915000000MHz SyncWord:18 20:45:38.156 -> SNo:01-23-8C-48-D6-D1-F5-86-EE 20:45:38.190 -> LoRa setup start 20:45:38.190 -> LoRa Setup done. 20:45:38.224 -> HM3301 setup start 20:45:38.292 -> HM3301 setup done 20:45:38.292 -> Setup done 20:45:38.292 -> 20:45:38.325 -> Loop called 20:45:38.325 -> PM1.5: 10ug/m3 20:45:38.359 -> PM2.5: 14ug/m3 20:45:38.359 -> PM10.0: 19ug/m3 20:45:38.393 -> Loop done 20:45:38.393 -> 20:46:38.220 -> Loop called 20:46:38.220 -> PM1.5: 10ug/m3 20:46:38.255 -> PM2.5: 15ug/m3 20:46:38.255 -> PM10.0: 20ug/m3 20:46:38.325 -> Loop done 20:46:38.325 -> 20:47:38.181 -> Loop called 20:47:38.181 -> PM1.5: 10ug/m3 20:47:38.181 -> PM2.5: 14ug/m3 20:47:38.216 -> PM10.0: 19ug/m3 20:47:38.250 -> Loop done 20:47:38.284 -> 20:48:38.123 -> Loop called 20:48:38.123 -> PM1.5: 10ug/m3 20:48:38.158 -> PM2.5: 14ug/m3 20:48:38.158 -> PM10.0: 19ug/m3 20:48:38.193 -> Loop done 20:48:38.227 -> 20:49:38.048 -> Loop called 20:49:38.082 -> PM1.5: 10ug/m3 20:49:38.082 -> PM2.5: 14ug/m3 20:49:38.117 -> PM10.0: 19ug/m3 20:49:38.151 -> Loop done 20:49:38.151 -> 20:50:38.010 -> Loop called 20:50:38.010 -> PM1.5: 9ug/m3 20:50:38.010 -> PM2.5: 13ug/m3 20:50:38.045 -> PM10.0: 18ug/m3 20:50:38.079 -> Loop done 20:50:38.079 ->
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 p10 for PM 1 value, p25 for PM2.5 value and p100 for PM10 value to the unique serial number from the ATSHA204A chip. (N.B. pay attention to the case of the field names they are case sensitive)

The rapidly settled into a narrow range of readings, but spiked when I took left it outside (winter in New Zealand) and the values spiked when food was being cooked in the kitchen which is next door to my office.
It would be good to run the sensor alongside a professional particulates monitor so the values could be compared and used to adjust the readings of the Grove sensor if necessary.


Bill of materials (prices as at August 2019)