Grove – Laser PM2.5 Sensor(HM3301) trial

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.

Seeeduino, Grove HM3301 and easysensors shield

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&gt;
#include <LoRa.h&gt;
#include <sha204_library.h&gt;
#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 &amp; 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(&amp;payload[payloadLength], to, toAddressLength);
  payloadLength += toAddressLength ;

  // Copy the "From" into payload
  memcpy(&amp;payload[payloadLength], from, fromAddressLength);
  payloadLength += fromAddressLength ;
}

void PayloadAdd( const char *sensorId, float value, byte decimalPlaces, bool last)
{
  byte sensorIdLength = strlen( sensorId ) ;

  memcpy( &amp;payload[payloadLength], sensorId,  sensorIdLength) ;
  payloadLength += sensorIdLength ;
  payload[ payloadLength] = SensorIdValueSeperator;
  payloadLength += 1 ;
  payloadLength += strlen( dtostrf(value, -1, decimalPlaces, (char *)&amp;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(&amp;payload[payloadLength], sensorId,  sensorIdLength) ;
  payloadLength += sensorIdLength ;
  payload[ payloadLength] = SensorIdValueSeperator;
  payloadLength += 1 ;
  payloadLength += strlen(itoa( value,(char *)&amp;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(&amp;payload[payloadLength], sensorId,  sensorIdLength) ;
  payloadLength += sensorIdLength ;
  payload[ payloadLength] = SensorIdValueSeperator;
  payloadLength += 1 ;
  payloadLength += strlen(utoa( value,(char *)&amp;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] &amp; 0xf ;
  byte toAddressLength = payload[0] &gt;&gt; 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)

Azure IoT Central telemetry configuration

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.

Hour of PM1, PM2.5 & PM10 readings in my office early evening
CO2 and particulates values while outside on my deck from 10:30pm to 11:30pm

Bill of materials (prices as at August 2019)

  • Seeeduino Nano USD6.90
  • Grove – Laser PM2.5 Sensor (HM3301) USD29.90
  • EasySensors Arduino Nano radio shield RFM95 USD15.00

Grove – Carbon Dioxide Sensor(SCD30) trial

In preparation for another student project to monitor the temperature, humidity and CO2 levels in a number of classrooms I purchased a couple of Grove – CO2, Temperature & Humidity Sensors (SCD30) for evaluation.

Seeeduino, Grove SCD30 and easysensors shield

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 seeedstudio wiki example calibration code, compiled and uploaded it to one of my Seeeduino Nano devices. When activated for the first time a period of minimum 7 days is needed so that the sensor algorithm can find its initial parameter set. During this period the sensor has to be exposed to fresh air for at least 1 hour every day.

During the calibration process I put the device in my garage and left the big door open for at least an hour every day. Once the sensor was calibrated I bought it inside at put it on the bookcase in my office.

I modified my Easy Sensors Arduino Nano Radio Shield RFM69/95 Payload Addressing client to use the sensor.

/*
  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&gt;
#include <LoRa.h&gt;
#include <sha204_library.h&gt;
#include "SCD30.h"

//#define DEBUG
//#define DEBUG_TELEMETRY
//#define DEBUG_LORA

// 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 = 300000;

// 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 &amp; humidity sensor
  Serial.println("SCD30 setup start");
  Wire.begin();
  scd30.initialize();  
  delay(100);
  Serial.println("SCD30 setup done");

  PayloadHeader((byte *)FieldGatewayAddress,strlen(FieldGatewayAddress), deviceSerialNumber, DeviceSerialNumberLength);

  Serial.println("Setup done");
  Serial.println();
}

void loop()
{
  unsigned long currentMilliseconds = millis();  
  float temperature ;
  float humidity ;
  float co2;

  Serial.println("Loop called");

  if(scd30.isAvailable())
  {
    float result[3] = {0};
    PayloadReset();

    // Read the CO2, temperature &amp; humidity values then display nicely
    scd30.getCarbonDioxideConcentration(result);

    co2 = result[0];
    Serial.print("C:");
    Serial.print(co2, 1) ;
    Serial.println("ppm ") ;

    PayloadAdd( "C", co2, 1, false);
    
    temperature = result[1];
    Serial.print("T:");
    Serial.print(temperature, 1) ;
    Serial.println("C ") ;

    PayloadAdd( "T", temperature, 1, false);

    humidity = result[2];
    Serial.print("H:" );
    Serial.print(humidity, 0) ;
    Serial.println("% ") ;

    PayloadAdd( "H", humidity, 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 ));
}
...
}    

The code is available on GitHub.

20:38:56.746 -> Setup called
20:38:56.746 -> Field gateway: Frequency:915000000MHz SyncWord:18
20:38:56.849 -> SNo:01-23-39-BD-D6-D1-F5-86-EE
20:38:56.884 -> LoRa setup start
20:38:56.919 -> LoRa Setup done.
20:38:56.919 -> SCD30 setup start
20:38:56.986 -> SCD30 setup done
20:38:56.986 -> Setup done
20:38:57.020 -> 
20:39:06.966 -> Received packet
20:39:06.966 -> Packet size:18
20:39:06.999 -> To len:9
20:39:06.999 -> From len:8
20:39:06.999 -> To:01-23-39-BD-D6-D1-F5-86-EE
20:39:07.034 -> From:4C-6F-52-61-49-6F-54-31
20:39:07.069 -> FieldGateway:4C-6F-52-61-49-6F-54-31
20:39:07.104 -> RSSI -55
20:39:07.139 -> Loop called
20:39:07.139 -> C:730.8ppm 
20:39:07.139 -> T:23.1C 
20:39:07.173 -> H:46% 
20:39:07.173 -> Loop done
20:39:07.208 -> 
20:39:37.123 -> Loop called
20:39:37.158 -> C:529.9ppm 
20:39:37.158 -> T:23.2C 
20:39:37.158 -> H:48% 
20:39:37.228 -> Loop done
20:39:37.228 -> 

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), h for the humidity % and t for the temperature °C to the unique serial number from the ATSHA204A chip. (N.B. pay attention to the case of the field names they are case sensitive)

Azure IoT Central telemetry configuration

Overall the performance of the sensor is looking pretty positive, the CO2 levels fluctuate in a acceptable range (based on office occupancy), and the temperature + humidity readings track quite closely to the other two sensor nodes in my office. The only issue so far is my lack of USB-C cables to power the devices in the field

CO2, Humidity and Temperature in my office for a day

Bill of materials (prices as at August 2019)

  • Seeeduino Nano USD6.90
  • Grove – CO2, Humidity & Temperature Sensor(SCD30) USD59.95
  • EasySensors Arduino Nano radio shield RFM95 USD15.00

IoT.Net LoRa Radio 915 MHz Payload Addressing client

This is a demo ingenuity micro IoT.Net client (based on one of the examples in my RFM9XLoRaNetMF library) that uploads telemetry data to my Windows 10 IoT Core on Raspberry PI field gateway. 

Thought the silk screen says RFM69 this is a prototype running an RFM95 module.

iotnetlora.jpg

Bill of materials (Prices Sep 2018)

  • IoT.Net device (Beta tester will add price when available)

The device has an onboard MCP9808 temperature sensor which kept the BoM really short. I have had to make some modifications to my RFM9XLoRaNetMF library as the IoT.Net device uses a different SPI port. The code for this devices and the changes will be uploaded to GitHub in the next couple of days.

//---------------------------------------------------------------------------------
// Copyright (c) Sept 2018, devMobile Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// git remote add origin https://github.com/KiwiBryn/FieldGateway.LoRa.IoTNetClient.git
// git push -u origin master
//---------------------------------------------------------------------------------
namespace devMobile.IoT.IoTNet.FieldGateway
{
	using System;
	using System.Text;
	using System.Threading;
	using Microsoft.SPOT;
	using Microsoft.SPOT.Hardware;
	using devMobile.IoT.NetMF.ISM;
	using IngenuityMicro.Sensors;

	class IoTNetClient
	{
		private readonly Rfm9XDevice rfm9XDevice;
		private readonly TimeSpan dueTime = new TimeSpan(0, 0, 10);
		private readonly TimeSpan periodTime = new TimeSpan(0, 0, 30);
		private readonly MCP9808 mcp9808 = new MCP9808();
		private readonly OutputPort _led = new OutputPort((Cpu.Pin)16 + 8, false);
		private readonly byte[] fieldGatewayAddress = Encoding.UTF8.GetBytes("LoRaIoT1");
		private readonly byte[] deviceAddress = Encoding.UTF8.GetBytes("IoTNet1");

		public IoTNetClient()
		{
			rfm9XDevice = new Rfm9XDevice( SPI.SPI_module.SPI3, (Cpu.Pin)16 + 9, (Cpu.Pin)5, (Cpu.Pin)4);
		}

		public void Run()
		{
			rfm9XDevice.Initialise(frequency: 915000000, paBoost: true, rxPayloadCrcOn: true);
			rfm9XDevice.Receive(deviceAddress);

			rfm9XDevice.OnDataReceived += rfm9XDevice_OnDataReceived;
			rfm9XDevice.OnTransmit += rfm9XDevice_OnTransmit;

			Timer temperatureUpdates = new Timer(TemperatureTimerProc, null, dueTime, periodTime);

			Thread.Sleep(Timeout.Infinite);
		}

		private void TemperatureTimerProc(object state)
		{
			_led.Write(true);

			double temperature = mcp9808.ReadTempInC();

			Debug.Print(DateTime.UtcNow.ToString("hh:mm:ss") + "  T:" + temperature.ToString("F1"));

			rfm9XDevice.Send(fieldGatewayAddress, Encoding.UTF8.GetBytes("t " + temperature.ToString("F1")));

			_led.Write(true);
		}

		void rfm9XDevice_OnTransmit()
		{
			Debug.Print("Transmit-Done");
			_led.Write(false);
		}

		void rfm9XDevice_OnDataReceived(byte[] address, float packetSnr, int packetRssi, int rssi, byte[] data)
		{
			try
			{
				string messageText = new string(UTF8Encoding.UTF8.GetChars(data));
				string addressText = new string(UTF8Encoding.UTF8.GetChars(address));

				Debug.Print(DateTime.UtcNow.ToString("HH:MM:ss") + "-Rfm9X PacketSnr " + packetSnr.ToString("F1") + " Packet RSSI " + packetRssi + "dBm RSSI " + rssi + "dBm = " + data.Length + " byte message " + @"""" + messageText + @"""");
			}
			catch (Exception ex)
			{
				Debug.Print(ex.Message);
			}
		}
	}
}
}

.Net Framework debug output Field Gateway

22:55:39-RX From IoTNet1 PacketSnr 9.5 Packet RSSI -50dBm RSSI -110dBm = 6 byte message "t 23.6"
 Sensor IoTNet1t Value 23.6
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish
The thread 0xbec has exited with code 0 (0x0).
The thread 0xbb4 has exited with code 0 (0x0).
The thread 0xa0c has exited with code 0 (0x0).
The thread 0x13c has exited with code 0 (0x0).
22:56:09-RX From IoTNet1 PacketSnr 9.3 Packet RSSI -44dBm RSSI -102dBm = 6 byte message "t 23.8"
 Sensor IoTNet1t Value 23.8
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish

A small footprint, battery powered .NetMF 4.4 LoRa device designed and made in New Zealand with Visual Studio 2017 support is great.

Netduino LoRa Radio 433/868/915 MHz Payload Addressing client

This is a demo Netduino client (based on one of the examples in my RFM9XLoRaNetMF library) that uploads telemetry data to my Windows 10 IoT Core on Raspberry PI field gateway proof of concept(PoC).

Bill of materials (Prices Sep 2018)

//---------------------------------------------------------------------------------
// Copyright (c) 2017, devMobile Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//---------------------------------------------------------------------------------
namespace devMobile.IoT.Netduino.FieldGateway
{
   using System;
   using System.Text;
   using System.Threading;
   using Microsoft.SPOT;
   using Microsoft.SPOT.Hardware;
   using SecretLabs.NETMF.Hardware.Netduino;
   using devMobile.IoT.NetMF.ISM;
   using devMobile.NetMF.Sensor;

   class NetduinoClient
   {
      Rfm9XDevice rfm9XDevice;
      private readonly TimeSpan dueTime = new TimeSpan(0, 0, 15);
      private readonly TimeSpan periodTime = new TimeSpan(0, 0, 300);
      private readonly SiliconLabsSI7005 sensor = new SiliconLabsSI7005();
      private readonly OutputPort _led = new OutputPort(Pins.ONBOARD_LED, false);
      private readonly byte[] fieldGatewayAddress = Encoding.UTF8.GetBytes("LoRaIoT1");
      private readonly byte[] deviceAddress = Encoding.UTF8.GetBytes("Netduino1");

      public NetduinoClient()
      {
         rfm9XDevice = new Rfm9XDevice(Pins.GPIO_PIN_D10, Pins.GPIO_PIN_D9, Pins.GPIO_PIN_D2);
      }

      public void Run()
      {
         //rfm9XDevice.Initialise(frequency: 915000000, paBoost: true, rxPayloadCrcOn: true);
         rfm9XDevice.Initialise(frequency: 433000000, paBoost: true, rxPayloadCrcOn: true);
         rfm9XDevice.Receive(deviceAddress);

         rfm9XDevice.OnDataReceived += rfm9XDevice_OnDataReceived;
         rfm9XDevice.OnTransmit += rfm9XDevice_OnTransmit;

         Timer humidityAndtemperatureUpdates = new Timer(HumidityAndTemperatureTimerProc, null, dueTime, periodTime);

         Thread.Sleep(Timeout.Infinite);
      }

      private void HumidityAndTemperatureTimerProc(object state)
      {
         _led.Write(true);

         double humidity = sensor.Humidity();
         double temperature = sensor.Temperature();

         Debug.Print(DateTime.UtcNow.ToString("hh:mm:ss") + " H:" + humidity.ToString("F1") + " T:" + temperature.ToString("F1"));

         rfm9XDevice.Send(fieldGatewayAddress, Encoding.UTF8.GetBytes( "t " + temperature.ToString("F1") + ",H " + humidity.ToString("F0")));

         _led.Write(true);
      }

      void rfm9XDevice_OnTransmit()
      {
         Debug.Print("Transmit-Done");
         _led.Write(false);
      }

      void rfm9XDevice_OnDataReceived(byte[] address, float packetSnr, int packetRssi, int rssi, byte[] data)
      {
         try
         {
            string messageText = new string(UTF8Encoding.UTF8.GetChars(data));
            string addressText = new string(UTF8Encoding.UTF8.GetChars(address));

            Debug.Print(DateTime.UtcNow.ToString("HH:MM:ss") + "-Rfm9X PacketSnr " + packetSnr.ToString("F1") + " Packet RSSI " + packetRssi + "dBm RSSI " + rssi + "dBm = " + data.Length + " byte message " + @"""" + messageText + @"""");
         }
         catch (Exception ex)
         {
            Debug.Print(ex.Message);
         }
      }
   }
}

The code is available on GitHub
FieldGatewayNetduinoLoRaElecrow915
Elecrow shield
FieldGatewayNetduinoLoRaDragino915
Dragino shield
FieldGatewayNetduinLoRaMakerFabs433
MakerFabs shield
Net Micro Framework debug output from device

The thread '' (0x2) has exited with code 0 (0x0).
12:00:18 H:96.9 T:19.6
Transmit-Done
12:05:17 H:95.1 T:20.1
Transmit-Done

.Net Framework debug output Field Gateway

The thread 0x1550 has exited with code 0 (0x0).
21:21:49-RX From Netduino1 PacketSnr 9.5 Packet RSSI -40dBm RSSI -107dBm = 11 byte message "t 19.6,H 97"
 Sensor Netduino1t Value 19.6
 Sensor Netduino1H Value 97
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish
...
21:26:49-RX From Netduino1 PacketSnr 9.5 Packet RSSI -33dBm RSSI -103dBm = 11 byte message "t 20.1,H 95"
 Sensor Netduino1t Value 20.1
 Sensor Netduino1H Value 95
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish
The thread 0xfbc has exited with code 0 (0x0).

Then in my Azure IoT Hub

AzureIOTHubExplorerScreenGrab20180917

M2M LoRaWan Node Model A328

Along with the M2M LoRaWan Gateway Shield for Raspberry Pi I also purchased a Low power LoRaWan Node Model A328. After setting the Board in Arduino IDE to Arduino pro mini 8Mhz 3V the device fired up and worked first time.

LoRaWanNodeV3_5
The device is intended for LoRaWan applications so the samples provided (including a link to application template generator) were not that applicable for my LoRa project so I used the Arduino LoRa library.

const int csPin = 10;          // LoRa radio chip select
const int resetPin = 9;       // LoRa radio reset
const int irqPin = 2;         // change for your board; must be a hardware interrupt pin

byte msgCount = 0;            // count of outgoing messages
int interval = 2000;          // interval between sends
long lastSendTime = 0;        // time of last packet send

void setup() {
  Serial.begin(9600);                   // initialize serial
  while (!Serial);

  Serial.println("LoRa Duplex - Set sync word");

  // override the default CS, reset, and IRQ pins (optional)
  LoRa.setPins(csPin, resetPin, irqPin);// set CS, reset, IRQ pin

  if (!LoRa.begin(915E6)) {             // initialize ratio at 915 MHz
    Serial.println("LoRa init failed. Check your connections.");
    while (true);                       // if failed, do nothing
  }

  LoRa.enableCrc();

  LoRa.setSyncWord(0x12);           // ranges from 0-0xFF, default 0x34, see API docs

  LoRa.dumpRegisters(Serial);
  Serial.println("LoRa init succeeded.");
}

void loop() {
  if (millis() - lastSendTime > interval) {
    String message = "0 Hello Arduino LoRa! ";   // send a message
    message += msgCount;
    sendMessage(message);
    Serial.println("Sending " + message);
    lastSendTime = millis();            // timestamp the message
    //interval = random(2000) + 1000;    // 2-3 seconds
    interval = 1000;
  }

  // parse for a packet, and call onReceive with the result:
  onReceive(LoRa.parsePacket());
}

void sendMessage(String outgoing) {
  LoRa.beginPacket();                   // start packet
  LoRa.print(outgoing);                 // add payload
  LoRa.endPacket();                     // finish packet and send it
  msgCount++;                           // increment message ID
}

void onReceive(int packetSize) {
  if (packetSize == 0) return;          // if there's no packet, return

  // read packet header bytes:
  String incoming = "";

  while (LoRa.available()) {
    incoming += (char)LoRa.read();
  }

  Serial.println("Message: " + incoming);
  Serial.println("RSSI: " + String(LoRa.packetRssi()));
  Serial.println("Snr: " + String(LoRa.packetSnr()));
  Serial.println();
}

I did find the “grove” connectors weren’t compatible with any of my sensors, but the vendor does include a number of cables DIY connection.

GroveConnectorIssue20180822

Next I’ll use power conservation modes and see how long I can get a set of AAA batteries to last. The device looks like a good option (esp. with solar power for devices with higher power consumption sensors) for some of the SmartAg projects my students are building.

In my Windows 10 IoT Core test application I could see the enableCrc() method was working according to the RegHopChannel CrcOnPayload flag.

For real deployments of the field gateway I think packets which have no CRC or a corrupted one will be dropped.

Re-reading the SX1276 datasheet

I sat down and read the Semtech SX1276 datasheet paying close attention to any references to CRCs and headers. Then to test some ideas I modified my Receive Basic test harness to see if I could reliably reproduce the problem with my stress test harness.LoRaStress2

public sealed class StartupTask : IBackgroundTask
	{
		private const int ChipSelectLine = 25;
		private const int ResetLine = 17;
		private Rfm9XDevice rfm9XDevice = new Rfm9XDevice(ChipSelectLine, ResetLine);

		public void Run(IBackgroundTaskInstance taskInstance)
		{
			// Put device into LoRa + Sleep mode
			rfm9XDevice.RegisterWriteByte(0x01, 0b10000000); // RegOpMode 

			// Set the frequency to 915MHz
			byte[] frequencyWriteBytes = { 0xE4, 0xC0, 0x00 }; // RegFrMsb, RegFrMid, RegFrLsb
			rfm9XDevice.RegisterWrite(0x06, frequencyWriteBytes);

			rfm9XDevice.RegisterWriteByte(0x0F, 0x0); // RegFifoRxBaseAddress 

			rfm9XDevice.RegisterWriteByte(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous

			while (true)
			{
				// Wait until a packet is received, no timeouts in PoC
				Debug.WriteLine("Receive-Wait");
				byte IrqFlags = rfm9XDevice.RegisterReadByte(0x12); // RegIrqFlags
				while ((IrqFlags & 0b01000000) == 0)  // wait until RxDone cleared
				{
					Task.Delay(20).Wait();
					IrqFlags = rfm9XDevice.RegisterReadByte(0x12); // RegIrqFlags
					Debug.Write(".");
				}
				Debug.WriteLine("");

				if ((IrqFlags & 0b00100000) == 0b00100000)
				{
					Debug.WriteLine("Payload CRC error");
				}

				byte regHopChannel = rfm9XDevice.RegisterReadByte(0x1C);
				Debug.WriteLine(string.Format("regHopChannel {0}", Convert.ToString((byte)regHopChannel, 2).PadLeft(8, '0')));

				byte currentFifoAddress = rfm9XDevice.RegisterReadByte(0x10); // RegFifiRxCurrent
				rfm9XDevice.RegisterWriteByte(0x0d, currentFifoAddress); // RegFifoAddrPtr*
				byte numberOfBytes = rfm9XDevice.RegisterReadByte(0x13); // RegRxNbBytes

				// Allocate buffer for message
				byte[] messageBytes = new byte[numberOfBytes];

				for (int i = 0; i < numberOfBytes; i++)
				{
					messageBytes[i] = rfm9XDevice.RegisterReadByte(0x00); // RegFifo
				}
				rfm9XDevice.RegisterWriteByte(0x12, 0xff); // RegIrqFlags clear all the bits

				string messageText = UTF8Encoding.UTF8.GetString(messageBytes);
				Debug.WriteLine("Received {0} byte message {1}", messageBytes.Length, messageText);
				Debug.WriteLine(string.Format("RegIrqFlags {0}", Convert.ToString((byte)IrqFlags, 2).PadLeft(8, '0')));
				Debug.WriteLine("Receive-Done");
			}
		}
	}

The RegHopChannel register has a flag indicating whether there was a CRC extracted from the packet header.

regHopChannel 00000000
Received 23 byte message 1 Hello Arduino LoRa! 1
RegIrqFlags 01010000
Receive-Done
Receive-Wait
…………………………..
regHopChannel 00000000
Received 23 byte message 1 Hello Arduino LoRa! 2
RegIrqFlags 01010000
Receive-Done
Receive-Wait
……………………………
regHopChannel 00000000
Received 23 byte message 1 Hello Arduino LoRa! 3
RegIrqFlags 01010000
Receive-Done
Receive-Wait

I then modified my Arduino-LoRa library based client to include a CRC

void setup() {
  Serial.begin(9600);                   // initialize serial
  while (!Serial);

  Serial.println("LoRa Duplex - Set sync word");

  // override the default CS, reset, and IRQ pins (optional)
  LoRa.setPins(csPin, resetPin, irqPin);// set CS, reset, IRQ pin

  if (!LoRa.begin(915E6)) {             // initialize ratio at 915 MHz
    Serial.println("LoRa init failed. Check your connections.");
    while (true);                       // if failed, do nothing
  }

  LoRa.enableCrc();  // BHL This was my change

  LoRa.setSyncWord(0x12);           // ranges from 0-0xFF, default 0x34, see API docs

  LoRa.dumpRegisters(Serial);
  Serial.println("LoRa init succeeded.");
}

void loop() {
  if (millis() - lastSendTime > interval) {
    String message = "5 Hello Arduino LoRa! ";   // send a message
    message += msgCount;
    sendMessage(message);
    Serial.println("Sending " + message);
    lastSendTime = millis();            // timestamp the message
    //interval = random(2000) + 1000;    // 2-3 seconds
    interval = 1000;
  }

  // parse for a packet, and call onReceive with the result:
  onReceive(LoRa.parsePacket());
}

void sendMessage(String outgoing) {
  LoRa.beginPacket();                   // start packet
  LoRa.print(outgoing);                 // add payload
  LoRa.endPacket();                     // finish packet and send it
  msgCount++;                           // increment message ID
}

void onReceive(int packetSize) {
  if (packetSize == 0) return;          // if there's no packet, return

  // read packet header bytes:
  String incoming = "";

  while (LoRa.available()) {
    incoming += (char)LoRa.read();
  }

  Serial.println("Message: " + incoming);
  Serial.println("RSSI: " + String(LoRa.packetRssi()));
  Serial.println("Snr: " + String(LoRa.packetSnr()));
  Serial.println();
}

When I powered up a single client and the payload had a CRC

...............................
regHopChannel 01000000
Received 23 byte message 6 Hello Arduino LoRa! 6
RegIrqFlags 01010000
Receive-Done
Receive-Wait
.................................
regHopChannel 01000000
Received 23 byte message 6 Hello Arduino LoRa! 7
RegIrqFlags 01010000
Receive-Done
Receive-Wait
.................................
regHopChannel 01000000
Received 23 byte message 6 Hello Arduino LoRa! 8
RegIrqFlags 01010000
Receive-Done
Receive-Wait
...............................

Then when I increased the number of clients I started getting corrupted messages with CRC errors.

Received 24 byte message 6 Hello Arduino LoRa! 32
RegIrqFlags 01010000
Receive-Done
Receive-Wait
...............
regHopChannel 01000001
Received 25 byte message 8 Hello Arduino LoRa! 114
RegIrqFlags 01010000
Receive-Done
Receive-Wait
Payload CRC error
regHopChannel 01000000
Received 24 byte message s��=��(��p�^j�\ʏ�����
RegIrqFlags 01100000
Receive-Done
Receive-Wait
.............
regHopChannel 01000000
Received 24 byte message 6 Hello Arduino LoRa! 33
RegIrqFlags 01010000
Receive-Done
Receive-Wait
...............
regHopChannel 01000001
Received 25 byte message 8 Hello Arduino LoRa! 115
RegIrqFlags 01010000
Receive-Done
Receive-Wait

I need to do some more testing but now I think the problem was the RegIrqFlags PayloadCRCError flag was never going to get set because there was no CRC on the payload.

Wireless field gateway protocol V2

I have now built a couple of nRF2L01P field gateways (for AdaFriut.IO & Azure IoT Hubs) which run as a background tasks on Windows 10 IoT Core on RaspberyPI). I have also written several clients which run on Arduino, devDuino, Netduino, and Seeeduino devices.

I have tried to keep the protocol simple (telemetry only) to deploy and it will be used in high school student projects in the next couple of weeks.

To make the payload smaller the first byte of the message now specifies the message type in the top nibble and the length of the device unique identifier in the bottom nibble.

0 = Echo

The message is displayed by the field gateway as text & hexadecimal.

1 = Device identifier + Comma separated values (CSV) payload

[0] – Set to 0001, XXXX   Device identifier length

[1]..[1+Device identifier length] – Unique device identifier bytes e.g. Mac address

[1+Device identifier length+1 ]..[31] – CSV payload e.g.  SensorID value, SensorID value