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

Grove Base Hat for Raspberry PI Zero Windows 10 IoT Core

During the week a package arrived from Seeedstudio with a Grove Base Hat for RPI Zero. So I have modified my Grove Base Hat for RPI Windows 10 IoT Core library to add support for the new shield.

Grove Base Hat for Raspberry PI Zero on Raspberry PI 3

The Raspberry PI Zero hat has a two less analog ports and a different device id so some conditional compile options were necessary

namespace devMobile.Windows10IoTCore.GroveBaseHatRPI
{
#if (!GROVE_BASE_HAT_RPI && !GROVE_BASE_HAT_RPI_ZERO)
#error Library must have at least one of GROVE_BASE_HAT_RPI or GROVE_BASE_HAT_RPI_ZERO defined
#endif

#if (GROVE_BASE_HAT_RPI && GROVE_BASE_HAT_RPI_ZERO)
#error Library must have at most one of GROVE_BASE_HAT_RPI or GROVE_BASE_HAT_RPI_ZERO defined
#endif

	public class AnalogPorts : IDisposable
	{
		private const int I2CAddress = 0x04;
		private const byte RegisterDeviceId = 0x0;
		private const byte RegisterVersion = 0x02;
		private const byte RegisterPowerSupplyVoltage = 0x29;
		private const byte RegisterRawBase = 0x10;
		private const byte RegisterVoltageBase = 0x20;
		private const byte RegisterValueBase = 0x30;
#if GROVE_BASE_HAT_RPI
		private const byte DeviceId = 0x0004;
#endif
#if GROVE_BASE_HAT_RPI_ZERO
		private const byte DeviceId = 0x0005;
#endif
		private I2cDevice Device= null;
		private bool Disposed = false;

		public enum AnalogPort
		{
			A0 = 0,
			A1 = 1,
			A2 = 2,
			A3 = 3,
			A4 = 4,
			A5 = 5,
#if GROVE_BASE_HAT_RPI
			A6 = 6,
			A7 = 7,
#endif
		};

The code updates have been “smoke” tested and I have updated the GitHub repository.

Grove – Carbon Dioxide Sensor(MH-Z16) trial

In preparation for a student project to monitor the CO2 levels in a number of classrooms I purchased a Grove – Carbon Dioxide Sensor(MH-Z16) for evaluation.


Arduino Uno R3 and CO2 Sensor

I downloaded the seeedstudio wiki example code, compiled and uploaded it to one of my Arduino Uno R3 devices.

I increased delay between readings to 10sec and reduced the baud rate of the serial logging to 9600baud.

/*
  This test code is write for Arduino AVR Series(UNO, Leonardo, Mega)
  If you want to use with LinkIt ONE, please connect the module to D0/1 and modify:

  // #include <SoftwareSerial.h>
  // SoftwareSerial s_serial(2, 3);      // TX, RX

  #define sensor Serial1
*/


#include <SoftwareSerial.h>
SoftwareSerial s_serial(2, 3);      // TX, RX

#define sensor s_serial

const unsigned char cmd_get_sensor[] =
{
    0xff, 0x01, 0x86, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x79
};

unsigned char dataRevice[9];
int temperature;
int CO2PPM;

void setup()
{
    sensor.begin(9600);
    Serial.begin(9600);
    Serial.println("get a 'g', begin to read from sensor!");
    Serial.println("********************************************************");
    Serial.println();
}

void loop()
{
    if(dataRecieve())
    {
        Serial.print("Temperature: ");
        Serial.print(temperature);
        Serial.print("  CO2: ");
        Serial.print(CO2PPM);
        Serial.println("");
    }
    delay(10000);
}

bool dataRecieve(void)
{
    byte data[9];
    int i = 0;

    //transmit command data
    for(i=0; i<sizeof(cmd_get_sensor); i++)
    {
        sensor.write(cmd_get_sensor[i]);
    }
    delay(10);
    //begin reveiceing data
    if(sensor.available())
    {
        while(sensor.available())
        {
            for(int i=0;i<9; i++)
            {
                data[i] = sensor.read();
            }
        }
    }

    for(int j=0; j<9; j++)
    {
        Serial.print(data[j]);
        Serial.print(" ");
    }
    Serial.println("");

    if((i != 9) || (1 + (0xFF ^ (byte)(data[1] + data[2] + data[3] + data[4] + data[5] + data[6] + data[7]))) != data[8])
    {
        return false;
    }

    CO2PPM = (int)data[2] * 256 + (int)data[3];
    temperature = (int)data[4] - 40;

    return true;
}

The debug output wasn’t too promising there weren’t any C02 parts per million (ppm) values and the response payloads looked wrong. So I downloaded the MH-Z16 NDIR CO2 Sensor datasheet for some background. The datasheet didn’t mention any temperature data in the message payloads so I removed that code.

The response payload validation code was all on one line and hard to figure out what it was doing.

    if((i != 9) || (1 + (0xFF ^ (byte)(data[1] + data[2] + data[3] + data[4] + data[5] + data[6] + data[7]))) != data[8])
    {
        return false;
    }

To make debugging easier I split the payload validation code into several steps so I could see what was failing.

/*
  This test code is write for Arduino AVR Series(UNO, Leonardo, Mega)
  If you want to use with LinkIt ONE, please connect the module to D0/1 and modify:

  // #include <SoftwareSerial.h>
  // SoftwareSerial s_serial(2, 3);      // TX, RX

  #define sensor Serial1
*/


#include <SoftwareSerial.h>
SoftwareSerial s_serial(2, 3);      // TX, RX

#define sensor s_serial

const unsigned char cmd_get_sensor[] =
{
    0xff, 0x01, 0x86, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x79
};

unsigned char dataRevice[9];
int CO2PPM;

void setup()
{
    sensor.begin(9600);
    Serial.begin(9600);
    Serial.println("get a 'g', begin to read from sensor!");
    Serial.println("********************************************************");
    Serial.println();
}

void loop()
{
    if(dataRecieve())
    {
        Serial.print("  CO2: ");
        Serial.print(CO2PPM);
        Serial.println("");
    }
    delay(10000);
}

bool dataRecieve(void)
{
    byte data[9];
    int i = 0;

    //transmit command data
    for(i=0; i<sizeof(cmd_get_sensor); i++)
    {
        sensor.write(cmd_get_sensor[i]);
    }
    delay(10);
    //begin reveiceing data
    if(sensor.available())
    {
        while(sensor.available())
        {
            for(int i=0;i<9; i++)
            {
                data[i] = sensor.read();
            }
        }
    }

    for(int j=0; j<9; j++)
    {
        Serial.print(data[j]);
        Serial.print(" ");
    }
    Serial.println("");

    // First calculate then validate the check sum as there is no point in proceeding if the packet is corrupted. (code inspired by datasheet algorithm)
    byte checksum = 0 ;
    for(int j=1; j<8; j++)
    {
      checksum += data[j];
    }
    checksum=0xff-checksum; 
    checksum+=1;
       
    if  (checksum != data[8])
    {
      Serial.println("Error checksum");
      return false;
    }

    // Then check the start byte to make sure response is what we were expecting
    if ( data[0] != 0xFF )
    {
        Serial.println("Error start byte");
        return false;
    }

    // Then check the command byte to make sure response is what we were expecting
    if ( data[1] != 0x86 )
    {
        Serial.println("Error command");
        return false;
    }


    CO2PPM = (int)data[2] * 256 + (int)data[3];

    return true;
}

From these modifications I could see the payload was messed up and based on the datasheet message descriptions it looked like it was offset by a byte or two.

15:58:32.509 -> get a 'g', begin to read from sensor!
15:58:32.578 -> ********************************************************
15:58:32.612 -> 
15:58:32.612 -> 255 134 6 238 76 0 0 1 255 
15:58:32.647 -> Error checksum
15:58:42.631 -> 57 255 134 6 246 76 0 0 1 
15:58:42.666 -> Error checksum
15:58:52.667 -> 49 255 134 5 125 76 0 0 1 
15:58:52.702 -> Error checksum
15:59:02.704 -> 171 255 134 4 86 76 0 0 1 
15:59:02.750 -> Error checksum

I had a look at the code and the delay(10) after sending the sensor reading request message caught my attention. I have found that often delay(x) commands are used to “tweak” the code to get it to work.

These “tweaks” often break when code is run on a different device or sensor firmware is updated changing the timing of individual bytes, or request-response processes.

I removed the delay(10) replaced it with a serial.flush() and changed the code to display the payload bytes in hexadecimal.

/*
  This test code is write for Arduino AVR Series(UNO, Leonardo, Mega)
  If you want to use with LinkIt ONE, please connect the module to D0/1 and modify:

  // #include <SoftwareSerial.h>
  // SoftwareSerial s_serial(2, 3);      // TX, RX

  #define sensor Serial1
*/


#include <SoftwareSerial.h>
SoftwareSerial s_serial(2, 3);      // TX, RX

#define sensor s_serial

const unsigned char cmd_get_sensor[] =
{
    0xff, 0x01, 0x86, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x79
};

unsigned char dataRevice[9];
int CO2PPM;

void setup()
{
    sensor.begin(9600);
    Serial.begin(9600);
    Serial.println("get a 'g', begin to read from sensor!");
    Serial.println("********************************************************");
    Serial.println();
}

void loop()
{
    if(dataRecieve())
    {
        Serial.print("  CO2: ");
        Serial.print(CO2PPM);
        Serial.println("");
    }
    delay(10000);
}

bool dataRecieve(void)
{
    byte data[9];
    int i = 0;

    //transmit command data
    for(i=0; i<sizeof(cmd_get_sensor); i++)
    {
        sensor.write(cmd_get_sensor[i]);
    }
    Serial.flush();
    
    //begin reveiceing data
    if(sensor.available())
    {
        while(sensor.available())
        {
            for(int i=0;i<9; i++)
            {
                data[i] = sensor.read();
            }
        }
    }

    for(int j=0; j<9; j++)
    {
        Serial.print(data[j],HEX);
        Serial.print(" ");
    }
    Serial.println("");

    // First calculate then validate the check sum as there is no point in proceeding if the packet is corrupted. (code inspired by datasheet algorithm)
    byte checksum = 0 ;
    for(int j=1; j<8; j++)
    {
      checksum += data[j];
    }
    checksum=0xff-checksum; 
    checksum+=1;
       
    if  (checksum != data[8])
    {
      Serial.println("Error checksum");
      return false;
    }

    // Then check the start byte to make sure response is what we were expecting
    if ( data[0] != 0xFF )
    {
        Serial.println("Error start byte");
        return false;
    }

    // Then check the command byte to make sure response is what we were expecting
    if ( data[1] != 0x86 )
    {
        Serial.println("Error command");
        return false;
    }


    CO2PPM = (int)data[2] * 256 + (int)data[3];

    return true;
}

The initial values from the sensor were a bit high, but after leaving the device running for 3 minutes (Preheat time in the documentation) they settled down into a reasonable range

16:14:31.686 -> get a 'g', begin to read from sensor!
16:14:31.721 -> ********************************************************
16:14:31.789 -> 
16:14:31.789 -> 255 134 6 224 75 0 0 1 72 
16:14:31.823 ->   CO2: 1760
16:14:41.824 -> 255 134 6 224 75 0 0 1 72 
16:14:41.824 ->   CO2: 1760
16:14:51.824 -> 255 134 5 189 75 0 0 1 108 
16:14:51.858 ->   CO2: 1469
16:15:01.868 -> 255 134 3 157 75 0 0 1 142 
16:15:01.868 ->   CO2: 925
16:15:11.857 -> 255 134 3 223 75 0 0 1 76 
16:15:11.892 ->   CO2: 991
16:15:21.882 -> 255 134 6 56 75 0 0 1 240 
16:15:21.917 ->   CO2: 1592
16:15:31.911 -> 255 134 4 186 75 0 0 1 112 
16:15:31.945 ->   CO2: 1210
16:15:41.927 -> 255 134 3 131 75 0 0 1 168 
16:15:41.962 ->   CO2: 899
16:15:51.940 -> 255 134 3 30 75 0 0 1 13 
16:15:51.975 ->   CO2: 798
16:16:01.986 -> 255 134 2 201 75 0 0 1 99 
16:16:01.986 ->   CO2: 713
16:16:11.985 -> 255 134 4 133 75 0 0 1 165 
16:16:12.019 ->   CO2: 1157
16:16:22.020 -> 255 134 6 62 75 0 0 1 234 
16:16:22.053 ->   CO2: 1598
16:16:32.041 -> 255 134 5 80 75 0 0 1 217 
16:16:32.041 ->   CO2: 1360
16:16:42.057 -> 255 134 3 204 75 0 0 1 95 
16:16:42.092 ->   CO2: 972
16:16:52.084 -> 255 134 3 191 75 0 0 1 108 
16:16:52.084 ->   CO2: 959
16:17:02.102 -> 255 134 2 230 75 0 0 1 70 
16:17:02.102 ->   CO2: 742
16:17:12.094 -> 255 134 3 106 75 0 0 1 193 
16:17:12.129 ->   CO2: 874
16:17:22.111 -> 255 134 2 227 75 0 0 1 73 
16:17:22.145 ->   CO2: 739
16:17:32.139 -> 255 134 3 225 75 0 0 1 74 
16:17:32.172 ->   CO2: 993
16:17:42.170 -> 255 134 3 109 75 0 0 1 190 
16:17:42.204 ->   CO2: 877
16:17:52.174 -> 255 134 2 188 75 0 0 1 112 
16:17:52.207 ->   CO2: 700
16:18:02.218 -> 255 134 2 70 75 0 0 1 230 
16:18:02.253 ->   CO2: 582
16:18:12.239 -> 255 134 2 163 75 0 0 1 137 
16:18:12.239 ->   CO2: 675
16:18:22.251 -> 255 134 2 110 75 0 0 1 190 
16:18:22.285 ->   CO2: 622
16:18:32.246 -> 255 134 2 83 75 0 0 1 217 
16:18:32.280 ->   CO2: 595
16:18:42.277 -> 255 134 2 48 75 0 0 1 252 
16:18:42.312 ->   CO2: 560
16:18:52.305 -> 255 134 2 62 75 0 0 1 238 
16:18:52.339 ->   CO2: 574

Bill of materials (prices as at Jan 2019)

After these tentative fixes for the MH-Z16 sensor I think going to see if there are any other libraries written by someone smarter than me available.

Grove Base Hat for Raspberry PI Windows 10 IoT Core

After some experimentation I have a proof of concept Windows 10 IoT Core library for accessing the Analog to Digital Convertor (ADC) on a Grove Base Hat for Raspberry PI.

I can read the raw, voltage & % values just fine but the Version number isn’t quite what I expected. In the python sample code I can see the register numbers etc.

def __init__(self, address=0x04):
self.address = address
self.bus = grove.i2c.Bus()

def read_raw(self, channel):
addr = 0x10 + channel
return self.read_register(addr)

# read input voltage (mV)
def read_voltage(self, channel):
addr = 0x20 + channel
return self.read_register(addr)

# input voltage / output voltage (%)
def read(self, channel):
addr = 0x30 + channel
return self.read_register(addr)

@property
def name(self):
id = self.read_register(0x0)
if id == RPI_HAT_PID:
return RPI_HAT_NAME
elif id == RPI_ZERO_HAT_PID:
return RPI_ZERO_HAT_NAME

@property
def version(self):
return self.read_register(0x3)

When I read register 0x3 to get the version info the value changes randomly. Format = register num, byte value, word value

0,4,4 1,134,10374 2,2,2 3,82,79 4,0,0 5,0,0 6,0,0 7,0,0 8,0,0 9,0,0 10,0,0 11,0,0 12,0,0 13,0,0 14,0,0 15,0,0 
0,4,4 1,134,10374 2,2,2 3,86,69 4,0,0 5,0,0 6,0,0 7,0,0 8,0,0 9,0,0 10,0,0 11,0,0 12,0,0 13,0,0 14,0,0 15,0,0 
0,4,4 1,134,10374 2,2,2 3,32,66 4,0,0 5,0,0 6,0,0 7,0,0 8,0,0 9,0,0 10,0,0 11,0,0 12,0,0 13,0,0 14,0,0 15,0,0 

It looks like register 1 or 2 (134/10374 or 2/2) might contain the device version information.

The code is available on GitHub here. Next time I purchase some gear from Seeedstudio I’ll include a Grove Base Hat For Raspberry PI Zero and extend the software so they work as well.

public sealed class StartupTask : IBackgroundTask
{
   private ThreadPoolTimer timer;
   private BackgroundTaskDeferral deferral;
   AnalogPorts analogPorts = new AnalogPorts();

   public void Run(IBackgroundTaskInstance taskInstance)
   {
      deferral = taskInstance.GetDeferral();

      analogPorts.Initialise();

      byte version = analogPorts.Version();
      Debug.WriteLine($"Version {version}");

      double powerSupplyVoltage = analogPorts.PowerSupplyVoltage();
      Debug.WriteLine($"Power supply voltage {powerSupplyVoltage}v");

      timer = ThreadPoolTimer.CreatePeriodicTimer(AnalogPorts, TimeSpan.FromSeconds(5));
   }

   void AnalogPorts(ThreadPoolTimer timer)
   {
      try
      {
         ushort valueRaw;
         valueRaw = analogPorts.ReadRaw(AnalogPorts.AnalogPort.A0);
         Debug.WriteLine($"A0 Raw {valueRaw}");

         double valueVoltage;
         valueVoltage = analogPorts.ReadVoltage(AnalogPorts.AnalogPort.A0);
         Debug.WriteLine($"A0 {valueVoltage}v");

         double value;
         value = analogPorts.Read(AnalogPorts.AnalogPort.A0);
         Debug.WriteLine($"A0 {value}");
      }
      catch (Exception ex)
      {
         Debug.WriteLine($"AnalogPorts Read failed {ex.Message}");
      }
   }
}

Grove Base Hat for Raspberry PI Investigation

For a couple of projects I had been using the Dexter industries GrovePI+ and the Grove Base Hat for Raspberry PI looked like a cheaper alternative for many applications, but it lacked Windows 10 IoT Core support.

My first project was to build a Inter Integrated Circuit(I2C) device scanner to check that the Grove Base Hat STM32 MCU I2C client implementation on a “played nice” with Windows 10 IoT core.

My Visual Studio 2017 project (I2C Device Scanner) scans all the valid 7bit I2C addresses and in the debug output displayed the two “found” devices, a Grove- 3 Axis Accelerometer(+-16G) (ADXL345) and the Grove Base Hat for Raspberry PI.

backgroundTaskHost.exe' (CoreCLR: CoreCLR_UWP_Domain): Loaded 'C:\Data\Users\DefaultAccount\AppData\Local\DevelopmentFiles\I2CDeviceScanner-uwpVS.Debug_ARM.Bryn.Lewis\System.Diagnostics.Debug.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.

'backgroundTaskHost.exe' (CoreCLR: CoreCLR_UWP_Domain): Loaded 'C:\Data\Users\DefaultAccount\AppData\Local\DevelopmentFiles\I2CDeviceScanner-uwpVS.Debug_ARM.Bryn.Lewis\System.Linq.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Exception thrown: 'System.IO.FileNotFoundException' in devMobile.Windows10IoTCore.I2CDeviceScanner.winmd
WinRT information: Slave address was not acknowledged.
.......
Exception thrown: 'System.IO.FileNotFoundException' in devMobile.Windows10IoTCore.I2CDeviceScanner.winmd
WinRT information: Slave address was not acknowledged.

I2C Controller \\?\ACPI#MSFT8000#1#{a11ee3c6-8421-4202-a3e7-b91ff90188e4}\I2C1 has 2 devices
Address 0x4
Address 0x53
Raspberry PI with Grove Base Hat & ADXL345 & Rotary angle sensor
Raspberry PI with Grove Base Hat I2C test rig

The next step was to confirm I could read the device ID of the ADXL345 and the Grove Base Hat for RaspberryPI. I had to figure out the Grove Base Hat for RaspberryPI from the Seeedstudio Python code.

I2CDevicePinger ADXL345 Debug output

...
'backgroundTaskHost.exe' (CoreCLR: CoreCLR_UWP_Domain): Loaded 'C:\Data\Users\DefaultAccount\AppData\Local\DevelopmentFiles\I2CDevicePinger-uwpVS.Debug_ARM.Bryn.Lewis\System.Diagnostics.Debug.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
DeviceID 0XE5

The DeviceID for the ADXL345 matched the DEVID in the device datasheet.

I2CDevicePinger Debug output

'backgroundTaskHost.exe' (CoreCLR: CoreCLR_UWP_Domain): Loaded 'C:\Data\Users\DefaultAccount\AppData\Local\DevelopmentFiles\I2CDevicePinger-uwpVS.Debug_ARM.Bryn.Lewis\System.Diagnostics.Debug.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
DeviceID 0X4

The DeviceID for the Grove Base Hat for RaspberryPI matched

RPI_HAT_PID = 0x0004 in the Python code.

The last test application reads the raw value of the specified analog input

public async void Run(IBackgroundTaskInstance taskInstance)
{
   string aqs = I2cDevice.GetDeviceSelector();
   DeviceInformationCollection I2CBusControllers = await DeviceInformation.FindAllAsync(aqs);

   if (I2CBusControllers.Count != 1)
   {
      Debug.WriteLine("Unexpect number of I2C bus controllers found");
      return;
   }

   I2cConnectionSettings settings = new I2cConnectionSettings(0x04)
   {
      BusSpeed = I2cBusSpeed.StandardMode,
      SharingMode = I2cSharingMode.Shared,
   };

   using (I2cDevice device = I2cDevice.FromIdAsync(I2CBusControllers[0].Id, settings).AsTask().GetAwaiter().GetResult())
   {
      try
      {
         ushort value = 0;
         // From the Seeedstudio python
	 // 0x10 ~ 0x17: ADC raw data
	 // 0x20 ~ 0x27: input voltage
         // 0x29: output voltage (Grove power supply voltage)
         // 0x30 ~ 0x37: input voltage / output voltage						
         do
	 {
            byte[] writeBuffer = new byte[1] { 0x10 };
            byte[] readBuffer = new byte[2] { 0, 0 };

            device.WriteRead(writeBuffer, readBuffer);
            value = BitConverter.ToUInt16(readBuffer, 0);

            Debug.WriteLine($"Value {value}");

            Task.Delay(1000).GetAwaiter().GetResult();
         }
         while (value != 0);
      }
      Catch (Exception ex)
      {
         Debug.WriteLine(ex.Message);
      }
   }
}

GroveBaseHatRPIRegisterReader Debug output

'backgroundTaskHost.exe' (CoreCLR: CoreCLR_UWP_Domain): Loaded 'C:\Data\Users\DefaultAccount\AppData\Local\DevelopmentFiles\GroveBaseHatRPIRegisterReader-uwpVS.Debug_ARM.Bryn.Lewis\System.Diagnostics.Debug.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Value 3685
Value 3685
Value 3688
Value 3681
Value 3681
Value 3688
Value 3688
Value 3683

The output changed when I adjusted the rotary angle sensor (0-4095) which confirmed I could reliably read the Analog input values.

The code for my test harness applications is available on github, the next step is to build a library for the Grove Base Hat for RaspberryPI

Arduino MKR1300 WAN Payload Addressing client

Last week a package arrived from SeeedStudio with some Arduino devices and Grove shields. With this gear I have built yet another client for my Azure IoT Hub and AdaFruit.IO  LoRa Field Gateways.

For my application I directly access the on-board Semtech SX127X chip by passing the Murata CMWX1ZZABZ functionality. To do this I (November 2018) I had to upgrade the device firmware using the Arduino updater.

Arduino MKR1300 WAN device with Grove Shield & patch antenna

The application is a modified version of my Arduino code with additional debugging support and payload formatting functionality.

/*
  Copyright ® 2018 November 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 <avr/dtostrf.h&gt;
#include <LoRa.h&gt;
#include <TH02_dev.h&gt;

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

// LoRa field gateway configuration (these settings must match your field gateway)
const char FieldGatewayAddress[] = {"LoRaIoT1"};
const char DeviceAddress[] = {"MKR1300LoRa1"};
const float FieldGatewayFrequency =  915000000.0;
const byte FieldGatewaySyncWord = 0x12 ;

// Payload configuration
const int InterruptPin = LORA_IRQ_DUMB;
const int ChipSelectPin = 6;
const int ResetPin = 1;

// LoRa radio payload configuration
const byte SensorIdValueSeperator = ' ' ;
const byte SensorReadingSeperator = ',' ;
const int LoopSleepDelaySeconds = 60 ;

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.println("LoRa setup start");
  
  // override the default chip select and reset pins
  LoRa.setPins(InterruptPin, ChipSelectPin, ResetPin); 
  if (!LoRa.begin(FieldGatewayFrequency))
  {
    Serial.println("LoRa begin failed");
    while (true); // Drop into endless loop requiring restart
  }

  // Need to do this so field gateways 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 TH02 temperature &amp; humidity sensor
  Serial.println("TH02 setup start");
  TH02.begin();
  delay(100);
  Serial.println("TH02 setup done");

  PayloadHeader((byte*)FieldGatewayAddress,strlen(FieldGatewayAddress), (byte*)DeviceAddress, strlen(DeviceAddress));

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


void loop()
{
  float temperature ;
  float humidity ;

  Serial.println("Loop called");

  PayloadReset();

  // Read the temperature &amp; humidity &amp; battery voltage values then display nicely
  temperature = TH02.ReadTemperature();
  Serial.print("T:");
  Serial.print( temperature, 1 ) ;
  Serial.println( "C " ) ;

  PayloadAdd( "T", temperature, 1);

  humidity = TH02.ReadHumidity();
  Serial.print("H:");
  Serial.print( humidity, 0 ) ;
  Serial.println( "% " ) ;

  PayloadAdd( "H", humidity, 0) ;

#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(LoopSleepDelaySeconds * 1000l);
}


void PayloadHeader( byte *to, byte toAddressLength, byte *from, byte fromAddressLength)
{
  byte addressesLength = toAddressLength + fromAddressLength ;

#ifdef DEBUG_TELEMETRY
  Serial.println("PayloadHeader- ");
  Serial.print( "To Address len:");
  Serial.print( toAddressLength );
  Serial.print( " From Address len:");
  Serial.print( fromAddressLength );
  Serial.print( " Addresses length:");
  Serial.print( addressesLength );
  Serial.println( );
#endif

  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( char *sensorId, float value, byte decimalPlaces)
{
  byte sensorIdLength = strlen( sensorId ) ;

#ifdef DEBUG_TELEMETRY
  Serial.println("PayloadAdd-float ");
  Serial.print( "SensorId:");
  Serial.print( sensorId );
  Serial.print( " sensorIdLen:");
  Serial.print( sensorIdLength );
  Serial.print( " Value:");
  Serial.print( value, decimalPlaces );
  Serial.print( " payloadLength:");
  Serial.print( payloadLength);
#endif

  memcpy( &amp;payload[payloadLength], sensorId,  sensorIdLength) ;
  payloadLength += sensorIdLength ;
  payload[ payloadLength] = SensorIdValueSeperator;
  payloadLength += 1 ;
  payloadLength += strlen( dtostrf(value, -1, decimalPlaces, (char *)&amp;payload[payloadLength]));
  payload[ payloadLength] = SensorReadingSeperator;
  payloadLength += 1 ;
  
#ifdef DEBUG_TELEMETRY
  Serial.print( " payloadLength:");
  Serial.print( payloadLength);
  Serial.println( );
#endif
}


void PayloadAdd( char *sensorId, int value )
{
  byte sensorIdLength = strlen( sensorId ) ;

#ifdef DEBUG_TELEMETRY
  Serial.println("PayloadAdd-int ");
  Serial.print( "SensorId:");
  Serial.print( sensorId );
  Serial.print( " sensorIdLen:");
  Serial.print( sensorIdLength );
  Serial.print( " Value:");
  Serial.print( value );
  Serial.print( " payloadLength:");
  Serial.print( payloadLength);
#endif  

  memcpy( &amp;payload[payloadLength], sensorId,  sensorIdLength) ;
  payloadLength += sensorIdLength ;
  payload[ payloadLength] = SensorIdValueSeperator;
  payloadLength += 1 ;
  payloadLength += strlen( itoa( value,(char *)&amp;payload[payloadLength],10));
  payload[ payloadLength] = SensorReadingSeperator;
  payloadLength += 1 ;
  
#ifdef DEBUG_TELEMETRY
  Serial.print( " payloadLength:");
  Serial.print( payloadLength);
  Serial.println( );
#endif
}


void PayloadAdd( char *sensorId, unsigned int value )
{
  byte sensorIdLength = strlen( sensorId ) ;

#ifdef DEBUG_TELEMETRY
  Serial.println("PayloadAdd-unsigned int ");
  Serial.print( "SensorId:");
  Serial.print( sensorId );
  Serial.print( " sensorIdLen:");
  Serial.print( sensorIdLength );
  Serial.print( " Value:");
  Serial.print( value );
  Serial.print( " payloadLength:");
  Serial.print( payloadLength);
#endif  

  memcpy( &amp;payload[payloadLength], sensorId,  sensorIdLength) ;
  payloadLength += sensorIdLength ;
  payload[ payloadLength] = SensorIdValueSeperator;
  payloadLength += 1 ;
  payloadLength += strlen( utoa( value,(char *)&amp;payload[payloadLength],10));
  payload[ payloadLength] = SensorReadingSeperator;
  payloadLength += 1 ;

#ifdef DEBUG_TELEMETRY
  Serial.print( " payloadLength:");
  Serial.print( payloadLength);
  Serial.println( );
#endif
}


void PayloadReset()
{
  byte fromAddressLength = payload[0] &amp; 0xf ;
  byte toAddressLength = payload[0] &gt;&gt; 4 ;
  byte addressesLength = toAddressLength + fromAddressLength ;

  payloadLength = addressesLength + 1;

#ifdef DEBUG_TELEMETRY
  Serial.println("PayloadReset- ");
  Serial.print( "To Address len:");
  Serial.print( toAddressLength );
  Serial.print( " From Address len:");
  Serial.print( fromAddressLength );
  Serial.print( " Addresses length:");
  Serial.print( addressesLength );
  Serial.println( );
#endif
}

After updating the firmware configuring the data to display in Azure IoT Central (or AdaFruit.IO) took minimal time.

Arduino MKR 1300 Data in Azure IoT Central

Bill of materials (Prices as at Nov 2018)

  • Arduino MKR WAN 1300 USD39.80
  • Arduino MKR Connection Carrier (Grove Compatible) USD22.80
  • Grove Temperature & Humidity Sensor USD11.50

So far the battery life is looking pretty good considering all I have done is used Delay to stop the loop method for 60 seconds.

Next steps are to see if I can retrieve a unique identifier from the Murata firmware and improve battery life by hibernating the processor etc.