TinyCLR OS V2 Seeed LoRa-E5 LoRaWAN library Part2

Nasty OTAA connect

After getting basic connectivity for my Seeedstudio LoRa-E5 Development Kit and Fezduino test rig working I wanted to see if I could get the device connected to The Things Industries(TTI) via the RAK7258 WisGate Edge Lite on the shelf in my office.

My Over the Air Activation (OTAA) implementation is very “nasty” as it is assumed that there are no timeouts or failures and it only sends one BCD message “48656c6c6f204c6f526157414e” which is “hello LoRaWAN”. The code just sequentially steps through the necessary commands (with a suitable delay after each is sent) to join the TTI network.

public class Program
{
#if TINYCLR_V2_FEZDUINO
   private static string SerialPortId = SC20100.UartPort.Uart5;
#endif
   private const string AppKey = "................................";

   //txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+ID=AppEui,{AppEui}\r\n"));
   //private const string AppEui = "................";

   //txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+ID=AppEui,\"{AppEui}\"\r\n"));
   private const string AppEui = ".. .. .. .. .. .. .. ..";

   private const byte messagePort = 1;

   //private const string payload = "48656c6c6f204c6f526157414e"; // Hello LoRaWAN
   private const string payload = "01020304"; // AQIDBA==
   //private const string payload = "04030201"; // BAMCAQ==

   public static void Main()
   {
      UartController serialDevice;
      int txByteCount;
      int rxByteCount;

      Debug.WriteLine("devMobile.IoT.SeeedE5.NetworkJoinOTAA starting");

      try
      {
         serialDevice = UartController.FromName(SerialPortId);

         serialDevice.SetActiveSettings(new UartSetting()
         {
            BaudRate = 9600,
            Parity = UartParity.None,
            StopBits = UartStopBitCount.One,
            Handshaking = UartHandshake.None,
            DataBits = 8
         });

         serialDevice.Enable();

         // Set the Region to AS923
         txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes("AT+DR=AS923\r\n"));
         Debug.WriteLine($"TX: DR {txByteCount} bytes");
         Thread.Sleep(500);

         // Read the response
         rxByteCount = serialDevice.BytesToRead;
         if (rxByteCount > 0)
         {
            byte[] rxBuffer = new byte[rxByteCount];
            serialDevice.Read(rxBuffer);
            Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
         }

         // Set the Join mode
         txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes("AT+MODE=LWOTAA\r\n"));
         Debug.WriteLine($"TX: MODE {txByteCount} bytes");
         Thread.Sleep(500);

         // Read the response
         rxByteCount = serialDevice.BytesToRead;
         if (rxByteCount > 0)
         {
            byte[] rxBuffer = new byte[rxByteCount];
            serialDevice.Read(rxBuffer);
            Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
         }

         // Set the appEUI
         txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+ID=AppEui,\"{AppEui}\"\r\n"));
         Debug.WriteLine($"TX: ID=AppEui {txByteCount} bytes");
         Thread.Sleep(500);

         // Read the response
         rxByteCount = serialDevice.BytesToRead;
         if (rxByteCount > 0)
         {
            byte[] rxBuffer = new byte[rxByteCount];
            serialDevice.Read(rxBuffer);
            Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
         }
            
         // Set the appKey
         txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+KEY=APPKEY,{AppKey}\r\n"));
         Debug.WriteLine($"TX: KEY=APPKEY {txByteCount} bytes");
         Thread.Sleep(500);

         // Read the response
         rxByteCount = serialDevice.BytesToRead;
         if (rxByteCount > 0)
         {
            byte[] rxBuffer = new byte[rxByteCount];
            serialDevice.Read(rxBuffer);
            Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
         }

         // Set the PORT
         txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+PORT={messagePort}\r\n"));
         Debug.WriteLine($"TX: PORT {txByteCount} bytes");
         Thread.Sleep(500);

         // Read the response
         rxByteCount = serialDevice.BytesToRead;
         if (rxByteCount > 0)
         {
            byte[] rxBuffer = new byte[rxByteCount];
            serialDevice.Read(rxBuffer);
            Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
         }

         // Join the network
         txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes("AT+JOIN\r\n"));
         Debug.WriteLine($"TX: JOIN {txByteCount} bytes");
         Thread.Sleep(10000);

         // Read the response
         rxByteCount = serialDevice.BytesToRead;
         if (rxByteCount > 0)
         {
            byte[] rxBuffer = new byte[rxByteCount];
            serialDevice.Read(rxBuffer);
            Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
         }

         while (true)
         {
            // Unconfirmed message
            txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+MSGHEX=\"{payload}\"\r\n"));
            Debug.WriteLine($"TX: MSGHEX {txByteCount} bytes");

            // Confirmed message
            //txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+CMSGHEX=\"{payload}\"\r\n"));
            //Debug.WriteLine($"TX: CMSGHEX {txByteCount} bytes");

            Thread.Sleep(10000);

            // Read the response
            rxByteCount = serialDevice.BytesToRead;
            if (rxByteCount > 0)
            {
               byte[] rxBuffer = new byte[rxByteCount];
               serialDevice.Read(rxBuffer);
               Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
            }

            Thread.Sleep(30000);
         }
      }
      catch (Exception ex)
      {
         Debug.WriteLine(ex.Message);
      }
   }
}

The code is not suitable for production but it confirmed my software and hardware configuration worked.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.IoT.SeeedE5.NetworkJoinOTAA starting
TX: DR 13 bytes
RX :+DR: AS923

TX: MODE 16 bytes
RX :+MODE: LWOTAA

TX: ID=AppEui 40 bytes
RX :+ID: AppEui, ..:..:.:.:.:.:.:.

TX: KEY=APPKEY 48 bytes
RX :+KEY: APPKEY ................................

TX: PORT 11 bytes
RX :+PORT: 1

TX: JOIN 9 bytes
RX :+JOIN: Start
+JOIN: NORMAL
+JOIN: Network joined
+JOIN: NetID 000013 DevAddr ..:..:..:..
+JOIN: Done

TX: MSGHEX 22 bytes
RX :+MSGHEX: Start
+MSGHEX: FPENDING
+MSGHEX: RXWIN1, RSSI -41, SNR 9.0
+MSGHEX: Done

TX: MSGHEX 22 bytes
RX :+MSGHEX: Start
+MSGHEX: Done

In the Visual Studio 2019 debug output I could see messages getting sent and then after a short delay they were visible in the TTI console.

Seeed E5 LoRaWAN dev Kit connecting in The Things Industries Device Live data tab

I had an issue with how the AppUI parameter was handled

   private const string AppKey = "................................";

   //txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+ID=AppEui,{AppEui}\r\n"));
   //private const string AppEui = "................";

   //txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+ID=AppEui,\"{AppEui}\"\r\n"));
   private const string AppEui = ".. .. .. .. .. .. .. ..";


It appears that If the appkey (or other string parameter) has spaces it has to be enclosed in quotations.

TinyCLR OS V2 Seeed LoRa-E5 LoRaWAN library Part1

Basic connectivity

Today I have been working on a GHI Electronics TinyCLR V2  C# library for the Seeedstudio LoRa-E5 module using my Seeedstudio LoRa-E5 Development Kit.

My initial test rig is based on an Fezduino board with a Grove Base Shield V2.0 connected to
LoRa-E5 Development Kit by a Grove – Universal 4 Pin 20cm Unbuckled Cable(TX/RX reversed)

Fezduino Seeed LoRaE5 test rig

The code has compile time options for synchronous and asynchronous operation.

public class Program
{
   private static UartController serialDevice;
   private const string ATCommand = "at+ver\r\n";
#if TINYCLR_V2_FEZDUINO
   private static readonly string SerialPortId = SC20100.UartPort.Uart5;
#endif
#if TINYCLR_V2_SC20100DEV_MIKROBUS_1
   private const string SerialPortId = SC20100.UartPort.Usart2;
#endif
#if TINYCLR_V2_SC20100DEV_MIKROBUS_2
   private const string SerialPortId = SC20100.UartPort.Uart3;
#endif

   public static void Main()
   {
      Debug.WriteLine("devMobile.IoT.SeeedE5.ShieldSerial starting");

      try
      {
         serialDevice = UartController.FromName(SerialPortId);

         serialDevice.SetActiveSettings(new UartSetting()
         {
            BaudRate = 9600,
            Parity = UartParity.None,
            StopBits = UartStopBitCount.One,
            Handshaking = UartHandshake.None,
            DataBits = 8
         });

         serialDevice.Enable();

#if SERIAL_ASYNC_READ
         serialDevice.DataReceived += SerialDevice_DataReceived;
#endif

         while (true)
         {
            byte[] txBuffer = UTF8Encoding.UTF8.GetBytes(ATCommand);

            int txByteCount = serialDevice.Write(txBuffer);
            Debug.WriteLine($"TX: {txByteCount} bytes");

#if SERIAL_SYNC_READ
            while( serialDevice.BytesToWrite>0)
            {
               Debug.WriteLine($" BytesToWrite {serialDevice.BytesToWrite}");
               Thread.Sleep(100);
            }

            int rxByteCount = serialDevice.BytesToRead;
            if (rxByteCount>0)
            {
               byte[] rxBuffer = new byte[rxByteCount];

               serialDevice.Read(rxBuffer);

               Debug.WriteLine($"RX sync:{rxByteCount} bytes read");
               String response = UTF8Encoding.UTF8.GetString(rxBuffer);
               Debug.WriteLine($"RX sync:{response}");
            }
#endif

            Thread.Sleep(20000);
         }
      }
      catch (Exception ex)
      {
         Debug.WriteLine(ex.Message);
      }
   }

#if SERIAL_ASYNC_READ
   private static void SerialDevice_DataReceived(UartController sender, DataReceivedEventArgs e)
   {
      byte[] rxBuffer = new byte[e.Count];

      sender.Read(rxBuffer, 0, e.Count);

      Debug.WriteLine($"RX Async:{e.Count} bytes read");
      String response = UTF8Encoding.UTF8.GetString(rxBuffer);
      Debug.WriteLine($"RX Async:{response}");
   }
#endif
}

When I first ran the code no data was received so I doubled checked the serial connections and figured out I had to modify the cable and reverse the TX and RX pins.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.IoT.SeeedLoRaE5.ShieldSerial starting
TX: 8 bytes
RX Async:1 bytes read
RX Async:+
RX Async:8 bytes read
RX Async:VER: 4.0
RX Async:5 bytes read
RX Async:.11

TX: 8 bytes
RX Async:1 bytes read
RX Async:+
RX Async:9 bytes read
RX Async:VER: 4.0.
RX Async:4 bytes read
RX Async:11

netNF Electric Longboard Part 3

Servo Control

The next step was to figure out how to operate a radio control(RC) servo as a proxy for an Electronic Speed Control(ESC).

My test rig uses (prices as at Aug 2020) the following parts

  • Netduino 3 Wifi
  • Grove-Base Shield V2.0 for Arduino USD4.45
  • Grove-Universal 4 Pin Bucked 20cm cable(5 PCs Pack) USD2.90
  • Grove-Servo USD5.90
  • Grove-Rotary Angle Sensor USD2.90

My servo test harness

public class Program
{
   public static void Main()
   {
      Debug.WriteLine("devMobile.Longboard.ServoTest starting");

      try
      {
         AdcController adc = AdcController.GetDefault();
         AdcChannel adcChannel = adc.OpenChannel(0);

         ServoMotor servo = new ServoMotor("TIM5", ServoMotor.ServoType.Positional, PinNumber('A', 0));
         servo.ConfigurePulseParameters(0.6, 2.3);

         while (true)
         {
            double value = adcChannel.ReadRatio();
            double position = Map(value, 0.0, 1.0, 0.0, 180);

            Debug.WriteLine($"Value: {value:F2} Position: {position:F1}");

            servo.Set(position);

            Thread.Sleep(100);
         }
      }
      catch (Exception ex)
      {
         Debug.WriteLine(ex.Message);
      }
   }

   private static int PinNumber(char port, byte pin)
   {
      if (port < 'A' || port > 'J')
         throw new ArgumentException();

      return ((port - 'A') * 16) + pin;
   }

   private static double Map(double x, double inputMinimum, double inputMaximum, double outputMinimum, double outputMaximum)
   {
      return (x - inputMinimum) * (outputMaximum - outputMinimum) / (inputMaximum - inputMinimum) + outputMinimum;
   }
}

The nanoFramework code polls for the rotary angle sensor for its position every 100mSec and then updates the servo.

The servo code was based on sample code provided by GHI Electronics for their TinyCLR which I had to adapt to work with the nanoFramework.

The next test rig will be getting the Netduino 3 software working my Longboard ESC and Lithium Polymer(LiPo) batteries.

netNF Electric Longboard Part 2

Analog Inputs & Pulse Width Modulation

The next step was to figure out how to configure a Pulse Width Modulation (PWM) output and an Analog Input so I could adjust the duty cycle and control the brightness of a Light Emitting Diode(LED).

Netduino 3 ADC & PWN test rig

My test rig uses (prices as at Aug 2020) the following parts

  • Netduino 3 Wifi
  • Grove-Base Shield V2.0 for Arduino USD4.45
  • Grove-Universal 4 Pin Bucked 5cm cable(5 PCs Pack) USD1.90
  • Grove-Universal 4 Pin Bucked 20cm cable(5 PCs Pack) USD2.90
  • Grove-LED Pack USD2.90
  • Grove-Rotary Angle Sensor USD2.90

My analog input test harness

 public class Program
   {
      public static void Main()
      {
         Debug.WriteLine("devMobile.Longboard.AdcTest starting");
         Debug.WriteLine(AdcController.GetDeviceSelector());

         try
         {
            AdcController adc = AdcController.GetDefault();
            AdcChannel adcChannel = adc.OpenChannel(0);

            while (true)
            {
               double value = adcChannel.ReadRatio();

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

               Thread.Sleep(100);
            }
         }
         catch (Exception ex)
         {
            Debug.WriteLine(ex.Message);
         }
      }
   }

The nanoFramework code polls for the rotary angle sensor for its position value every 100mSec.

The setup to use for the Analog to Digital Convertor(ADC) port was determined by looking at the board.h and target_windows_devices_adc_config.cpp file.

//
// Copyright (c) 2018 The nanoFramework project contributors
// See LICENSE file in the project root for full license information.
//

#include <win_dev_adc_native_target.h>

const NF_PAL_ADC_PORT_PIN_CHANNEL AdcPortPinConfig[] = {
    
    // ADC1
    {1, GPIOC, 0, ADC_CHANNEL_IN10},
    {1, GPIOC, 1, ADC_CHANNEL_IN11},

    // ADC2
    {2, GPIOC, 2, ADC_CHANNEL_IN14},
    {2, GPIOC, 3, ADC_CHANNEL_IN15},

    // ADC3
    {3, GPIOC, 4, ADC_CHANNEL_IN12},
    {3, GPIOC, 5, ADC_CHANNEL_IN13},

    // these are the internal sources, available only at ADC1
    {1, NULL, 0, ADC_CHANNEL_SENSOR},
    {1, NULL, 0, ADC_CHANNEL_VREFINT},
    {1, NULL, 0, ADC_CHANNEL_VBAT},
};

const int AdcChannelCount = ARRAYSIZE(AdcPortPinConfig);

The call to AdcController.GetDeviceSelector() only returned one controller

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.Longboard.AdcTest starting
ADC1

After some experimentation it appears that only A0 & A1 work on a Netduino. (Aug 2020).

My PWM test harness

public class Program
{
   public static void Main()
   {
      Debug.WriteLine("devMobile.Longboard.PwmTest starting");
      Debug.WriteLine(PwmController.GetDeviceSelector());

      try
      {
         PwmController pwm = PwmController.FromId("TIM5");
         AdcController adc = AdcController.GetDefault();
         AdcChannel adcChannel = adc.OpenChannel(0);

         PwmPin pwmPin = pwm.OpenPin(PinNumber('A', 0));
         pwmPin.Controller.SetDesiredFrequency(1000);
         pwmPin.Start();

         while (true)
         {
            double value = adcChannel.ReadRatio();

            Debug.WriteLine(value.ToString("F2"));

            pwmPin.SetActiveDutyCyclePercentage(value);

            Thread.Sleep(100);
         }
      }
      catch (Exception ex)
      {
         Debug.WriteLine(ex.Message);
      }
   }

   private static int PinNumber(char port, byte pin)
   {
      if (port < 'A' || port > 'J')
         throw new ArgumentException();
      return ((port - 'A') * 16) + pin;
   }
}

I had to refer to the Netduino schematic to figure out pin mapping

With my test rig (with easy access to D0 thru D8) I found that only D2,D3,D7 and D8 work as PWM outputs.

The next test rig will be getting Servo working.

netNF Electric Longboard Part 1

Wiichuck connectivity

Roughly four years ago I build myself an electric longboard as summer transport. It initially had a controller built with a devDuino V2.2 which after a while I “upgraded” to a GHI Electronics .NET Microframework device.

Configuring the original netMF based longboard

Now that GHI Electronics no longer supports the FEZ Panda III I figured upgrading to a device that runs the nanoFramework would be a good compromise.

I control the speed of the longboard with a generic wireless wii nunchuk. So my first project is porting the .NET Micro Framework Toolbox code to the nanoFramework.

wireless controller test rig

My test rig uses (prices as at Aug 2020) the following parts

  • Netduino 3 Wifi
  • Grove-Base Shield V2.0 for Arduino USD4.45
  • Grove-Universal 4 Pin Bucked 5cm cable(5 PCs Pack) USD1.90
  • Grove-Nunchuck USD2.90
  • Generic wireless WII nunchuk

My changes were mainly related to the Inter Integrated Circuit(I2C) configuration and the reading+writing of registers.

/// <summary>
/// Initialises a new Wii Nunchuk
/// </summary>
/// <param name="busId">The unique identifier of the I²C to use.</param>
/// <param name="slaveAddress">The I²C address</param>
/// <param name="busSpeed">The bus speed, an enumeration that defaults to StandardMode</param>
/// <param name="sharingMode">The sharing mode, an enumeration that defaults to Shared.</param>
public WiiNunchuk(string busId, ushort slaveAddress = 0x52, I2cBusSpeed busSpeed = I2cBusSpeed.StandardMode, I2cSharingMode sharingMode = I2cSharingMode.Shared)
   {
      I2cTransferResult result;

      // This initialisation routine seems to work. I got it at http://wiibrew.org/wiki/Wiimote/Extension_Controllers#The_New_Way
      Device = I2cDevice.FromId(busId, new I2cConnectionSettings(slaveAddress)
      {
         BusSpeed = busSpeed,
         SharingMode = sharingMode,
      });

      result = Device.WritePartial(new byte[] { 0xf0, 0x55 });
      if (result.Status != I2cTransferStatus.FullTransfer)
      {
         throw new ApplicationException("Something went wrong reading the Nunchuk. Did you use proper pull-up resistors?");
      }

      result = Device.WritePartial(new byte[] { 0xfb, 0x00 });
      if (result.Status != I2cTransferStatus.FullTransfer)
      {
         throw new ApplicationException("Something went wrong reading the Nunchuk. Did you use proper pull-up resistors?");
      }

      this.Device.Write(new byte[] { 0xf0, 0x55 });
      this.Device.Write(new byte[] { 0xfb, 0x00 });
   }

   /// <summary>
   /// Reads all data from the nunchuk
   /// </summary>
   public void Read()
   {
      byte[] WaitWriteBuffer = { 0 };
      I2cTransferResult result;

      result = Device.WritePartial(WaitWriteBuffer);
      if (result.Status != I2cTransferStatus.FullTransfer)
      {
         throw new ApplicationException("Something went wrong reading the Nunchuk. Did you use proper pull-up resistors?");
      }

      byte[] ReadBuffer = new byte[6];
      result = Device.ReadPartial(ReadBuffer);
      if (result.Status != I2cTransferStatus.FullTransfer)
      {
         throw new ApplicationException("Something went wrong reading the Nunchuk. Did you use proper pull-up resistors?");
      }

      // Parses data according to http://wiibrew.org/wiki/Wiimote/Extension_Controllers/Nunchuck#Data_Format

      // Analog stick
      this.AnalogStickX = ReadBuffer[0];
      this.AnalogStickY = ReadBuffer[1];

      // Accelerometer
      ushort AX = (ushort)(ReadBuffer[2] << 2);
      ushort AY = (ushort)(ReadBuffer[3] << 2);
      ushort AZ = (ushort)(ReadBuffer[4] << 2);
      AZ += (ushort)((ReadBuffer[5] & 0xc0) >> 6); // 0xc0 = 11000000
      AY += (ushort)((ReadBuffer[5] & 0x30) >> 4); // 0x30 = 00110000
      AX += (ushort)((ReadBuffer[5] & 0x0c) >> 2); // 0x0c = 00001100
      this.AcceleroMeterX = AX;
      this.AcceleroMeterY = AY;
      this.AcceleroMeterZ = AZ;

      // Buttons
      ButtonC = (ReadBuffer[5] & 0x02) != 0x02;    // 0x02 = 00000010
      ButtonZ = (ReadBuffer[5] & 0x01) != 0x01;    // 0x01 = 00000001
}

The nanoFramework code polls for the joystick position and accelerometer values every 100mSec

public class Program
{
   public static void Main()
   {
      Debug.WriteLine("devMobile.Longboard.WiiNunchuckTest starting");
      Debug.WriteLine(I2cDevice.GetDeviceSelector());

      try
      {
         WiiNunchuk nunchuk = new WiiNunchuk("I2C1");

         while (true)
         {
            nunchuk.Read();

            Debug.WriteLine($"JoyX: {nunchuk.AnalogStickX} JoyY:{nunchuk.AnalogStickY} AX:{nunchuk.AcceleroMeterX} AY:{nunchuk.AcceleroMeterY} AZ:{nunchuk.AcceleroMeterZ} BtnC:{nunchuk.ButtonC} BtnZ:{nunchuk.ButtonZ}");

            Thread.Sleep(100);
         }
      }
      catch (Exception ex)
      {
         Debug.WriteLine(ex.Message);
      }
   }
}

The setup to use for the I2C port was determined by looking at the board.h and target_windows_devices_I2C_config.cpp file

//
// Copyright (c) 2018 The nanoFramework project contributors
// See LICENSE file in the project root for full license information.
//

#include <win_dev_i2c_native_target.h>

//////////
// I2C1 //
//////////

// pin configuration for I2C1
// port for SCL pin is: GPIOB
// port for SDA pin is: GPIOB
// SCL pin: is GPIOB_6
// SDA pin: is GPIOB_7
// GPIO alternate pin function is 4 (see alternate function mapping table in device datasheet)
I2C_CONFIG_PINS(1, GPIOB, GPIOB, 6, 7, 4)

Then checking this against the Netduino 3 Wifi schematic.

This image has an empty alt attribute; its file name is netduinoschematic-1.jpg

After some experimentation with how to detect if an I2C read or write had failed the debugging console output began displaying reasonable value

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.Longboard.WiiNunchuckTest starting
I2C1
JoyX: 128 JoyY:128 AX:520 AY:508 AZ:708 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:520 AY:504 AZ:716 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:524 AY:508 AZ:716 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:524 AY:536 AZ:708 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:516 AY:528 AZ:724 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:492 AY:524 AZ:720 BtnC:True BtnZ:False
JoyX: 128 JoyY:128 AX:508 AY:528 AZ:700 BtnC:True BtnZ:False
JoyX: 128 JoyY:128 AX:504 AY:532 AZ:716 BtnC:True BtnZ:False
JoyX: 128 JoyY:128 AX:512 AY:532 AZ:724 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:516 AY:532 AZ:712 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:520 AY:532 AZ:708 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:524 AY:532 AZ:708 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:480 AY:504 AZ:688 BtnC:True BtnZ:True
JoyX: 128 JoyY:128 AX:480 AY:520 AZ:728 BtnC:False BtnZ:True
JoyX: 128 JoyY:128 AX:512 AY:520 AZ:704 BtnC:False BtnZ:True
JoyX: 128 JoyY:128 AX:512 AY:548 AZ:708 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:504 AY:516 AZ:728 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:548 AY:536 AZ:704 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:500 AY:528 AZ:728 BtnC:True BtnZ:False
JoyX: 128 JoyY:128 AX:496 AY:524 AZ:716 BtnC:True BtnZ:False
JoyX: 128 JoyY:128 AX:528 AY:536 AZ:696 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:540 AY:540 AZ:720 BtnC:False BtnZ:False
JoyX: 128 JoyY:128 AX:500 AY:520 AZ:684 BtnC:False BtnZ:False
JoyX: 128 JoyY:0 AX:520 AY:508 AZ:696 BtnC:False BtnZ:False
JoyX: 29 JoyY:0 AX:488 AY:576 AZ:716 BtnC:False BtnZ:False
JoyX: 0 JoyY:128 AX:532 AY:540 AZ:700 BtnC:False BtnZ:False
JoyX: 0 JoyY:128 AX:492 AY:512 AZ:708 BtnC:False BtnZ:False
JoyX: 0 JoyY:128 AX:492 AY:516 AZ:708 BtnC:False BtnZ:False
JoyX: 0 JoyY:128 AX:504 AY:512 AZ:708 BtnC:False BtnZ:False
JoyX: 27 JoyY:128 AX:508 AY:520 AZ:700 BtnC:False BtnZ:False
JoyX: 106 JoyY:128 AX:504 AY:516 AZ:700 BtnC:False BtnZ:False
JoyX: 0 JoyY:128 AX:496 AY:520 AZ:700 BtnC:False BtnZ:False
JoyX: 0 JoyY:128 AX:512 AY:532 AZ:716 BtnC:False BtnZ:False
JoyX: 0 JoyY:128 AX:500 AY:516 AZ:708 BtnC:False BtnZ:False
JoyX: 85 JoyY:113 AX:500 AY:536 AZ:720 BtnC:False BtnZ:False
JoyX: 128 JoyY:110 AX:512 AY:532 AZ:712 BtnC:False BtnZ:False
JoyX: 128 JoyY:90 AX:516 AY:528 AZ:716 BtnC:False BtnZ:False
JoyX: 128 JoyY:43 AX:508 AY:468 AZ:660 BtnC:False BtnZ:False
JoyX: 128 JoyY:0 AX:508 AY:532 AZ:712 BtnC:False BtnZ:False
JoyX: 128 JoyY:0 AX:496 AY:524 AZ:716 BtnC:False BtnZ:False

The next test rig will be getting Pulse Width Modulation(PWM) working.

Grove-VOC and eCO2 Gas Sensor (SGP30)

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

Seeeduino Nano easySensors shield and Grove VOC & eCO2 Sensor

Seeeduino Nano devices have a single on-board I2C socket which meant I didn’t need a Grove Shield for Arduino Nano which reduced the size and cost of the sensor node.

I downloaded the sample code from the Seeedstudio wiki and modified my Easy Sensors Arduino Nano Radio Shield RFM69/95 Payload Addressing client to use the sensor.

My first attempt failed with an issues accessing an Analog port to read the serial number from the Microchip ATSHA204 security chip. After looking at the Seeed SGP30 library source code (based on Sensiron samples) I think the my Nano device was running out of memory. I then searched for other Arduino compatible SGP30 libraries and rebuilt he application with the one from Sparkfun,

/*
  Copyright ® 2019 August devMobile Software, All Rights Reserved

  THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
  PURPOSE.

  You can do what you want with this code, acknowledgment would be nice.
  http://www.devmobile.co.nz

  Seeedstudio Grove - VOC and eCO2 Gas Sensor (SGP30)
  https://www.seeedstudio.com/Grove-VOC-and-eCO2-Gas-Sensor-SGP30-p-3071.html

  Seeeduino Nano 
  https://www.seeedstudio.com/Seeeduino-Nano-p-4111.html
  
  Polycarbonate enclosure approx 3.5" x 4.5"
    2 x Cable glands
    1 x Grommet to seal SMA antenna connector
    3M command adhesive strips to hold battery & device in place
   
*/
#include <stdlib.h>
#include "SparkFun_SGP30_Arduino_Library.h" 
#include <LoRa.h>
#include <sha204_library.h>

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

#define UNITS_VOC "ppb"
#define UNITS_CO2 "ppm"

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

// Payload configuration
const int ChipSelectPin = 10;
const int ResetPin = 9;
const int InterruptPin = 2;

// LoRa radio payload configuration
const byte SensorIdValueSeperator = ' ' ;
const byte SensorReadingSeperator = ',' ;
const unsigned long SensorUploadDelay = 60000;

// ATSHA204 secure authentication, validation with crypto and hashing (currently only using for unique serial number)
const byte Atsha204Port = A3;
atsha204Class sha204(Atsha204Port);
const byte DeviceSerialNumberLength = 9 ;
byte deviceSerialNumber[DeviceSerialNumberLength] = {""};

SGP30 mySensor; //create an object of the SGP30 class

const byte PayloadSizeMaximum = 64 ;
byte payload[PayloadSizeMaximum];
byte payloadLength = 0 ;


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

#ifdef DEBUG
  while (!Serial);
#endif
 
  Serial.println("Setup called");

  Serial.print("Field gateway:");
  Serial.print(FieldGatewayAddress ) ;
  Serial.print(" Frequency:");
  Serial.print( FieldGatewayFrequency,0 ) ;
  Serial.print("MHz SyncWord:");
  Serial.print( FieldGatewaySyncWord ) ;
  Serial.println();
  
   // Retrieve the serial number then display it nicely
  if(sha204.getSerialNumber(deviceSerialNumber))
  {
    Serial.println("sha204.getSerialNumber failed");
    while (true); // Drop into endless loop requiring restart
  }

  Serial.print("SNo:");
  DisplayHex( deviceSerialNumber, DeviceSerialNumberLength);
  Serial.println();

  Serial.println("LoRa setup start");

  // override the default chip select and reset pins
  LoRa.setPins(ChipSelectPin, ResetPin, InterruptPin);
  if (!LoRa.begin(FieldGatewayFrequency))
  {
    Serial.println("LoRa begin failed");
    while (true); // Drop into endless loop requiring restart
  }

  // Need to do this so field gateway pays attention to messsages from this device
  LoRa.enableCrc();
  LoRa.setSyncWord(FieldGatewaySyncWord);

#ifdef DEBUG_LORA
  LoRa.dumpRegisters(Serial);
#endif
  Serial.println("LoRa Setup done.");

  // Configure the DF Robot SHT20, temperature & humidity sensor
  Serial.println("SGP30 setup start");  
  Wire.begin();
  if(mySensor.begin() == false)
  {
    Serial.println("SQP-30 initialisation failed");
    while (true); // Drop into endless loop requiring restart
  }
  mySensor.initAirQuality();
  delay(1000);  
  Serial.println("SGP30 setup done");

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

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


void loop()
{
  unsigned long currentMilliseconds = millis();  

  Serial.println("Loop called");

  mySensor.measureAirQuality();

  PayloadReset();  

  PayloadAdd( "v", mySensor.TVOC, false);
     
  PayloadAdd( "c", mySensor.CO2, false);
  
  #ifdef DEBUG_VOC_AND_CO2  
    Serial.print("VoC:");
    Serial.print( mySensor.TVOC ) ;
    Serial.print( UNITS_VOC ) ;
    Serial.print(" Co2:");
    Serial.print( mySensor.CO2 ) ;
    Serial.println( UNITS_CO2 ) ;
  #endif

  #ifdef DEBUG_TELEMETRY
    Serial.println();
    Serial.print("RFM9X/SX127X Payload length:");
    Serial.print(payloadLength);
    Serial.println(" bytes");
  #endif

  LoRa.beginPacket();
  LoRa.write(payload, payloadLength);
  LoRa.endPacket();

  Serial.println("Loop done");
  Serial.println();

  delay(SensorUploadDelay - (millis() - currentMilliseconds ));
}


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

  payloadLength = 0 ;

  // prepare the payload header with "To" Address length (top nibble) and "From" address length (bottom nibble)
  
  payload[payloadLength] = (toAddressLength << 4) | fromAddressLength ;
  payloadLength += 1;

  // Copy the "To" address into payload
  memcpy(&payload[payloadLength], to, toAddressLength);
  payloadLength += toAddressLength ;

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


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

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


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

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


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

  memcpy(&payload[payloadLength], sensorId,  sensorIdLength) ;
  payloadLength += sensorIdLength ;
  payload[ payloadLength] = SensorIdValueSeperator;
  payloadLength += 1 ;
  payloadLength += strlen(utoa( value,(char *)&payload[payloadLength],10));
  if (!last)
  {
    payload[ payloadLength] = SensorReadingSeperator;
    payloadLength += 1 ;
  }
  
#ifdef DEBUG_TELEMETRY
  Serial.print("PayloadAdd uint-payloadLength:");
  Serial.print(payloadLength);
  Serial.println( );
#endif
}


void PayloadReset()
{
  byte fromAddressLength = payload[0] & 0xf ;
  byte toAddressLength = payload[0] >> 4 ;
  
  payloadLength = toAddressLength + fromAddressLength + 1;
}


void DisplayHex( byte *byteArray, byte length) 
{
  for (int i = 0; i < length ; i++)
  {
    // Add a leading zero
    if ( byteArray[i] < 16)
    {
      Serial.print("0");
    }
    Serial.print(byteArray[i], HEX);
    if ( i < (length-1)) // Don't put a - after last digit
    {
      Serial.print("-");
    }
  }
}    

The code is available on GitHub.

11:32:52.947 -> Setup called
11:32:52.947 -> Field gateway:LoRaIoT1 Frequency:915000000MHz SyncWord:18
11:32:53.085 -> SNo:01-23-21-61-D6-D1-F5-86-EE
11:32:53.118 -> LoRa setup start
11:32:53.118 -> LoRa Setup done.
11:32:53.153 -> SGP30 setup start
11:32:54.083 -> SGP30 setup done
11:32:54.117 -> Setup done
11:32:54.117 -> 
11:32:54.117 -> Loop called
11:32:54.152 -> VoC:0ppb Co2:400ppm
11:32:54.187 -> Loop done
11:32:54.187 -> 
11:33:54.092 -> Loop called
11:33:54.127 -> VoC:0ppb Co2:400ppm
11:33:54.195 -> Loop done
11:33:54.195 -> 
11:34:54.098 -> Loop called
11:34:54.133 -> VoC:17ppb Co2:425ppm
11:34:54.201 -> Loop done
11:34:54.201 -> 
11:35:54.109 -> Loop called
11:35:54.142 -> VoC:11ppb Co2:421ppm
11:35:54.210 -> Loop done
11:35:54.210 -> 
11:36:54.109 -> Loop called
11:36:54.143 -> VoC:3ppb Co2:409ppm
11:36:54.212 -> Loop done
11:36:54.212 -> 
11:37:54.135 -> Loop called
11:37:54.135 -> VoC:12ppb Co2:400ppm
11:37:54.204 -> Loop done
11:37:54.204 -> 
11:38:54.126 -> Loop called
11:38:54.161 -> VoC:11ppb Co2:439ppm
11:38:54.231 -> Loop done

To configure the device in Azure IoT Central (similar process for Adafruit.IO, working on support for losant, ubidots and MyDevices) I copied the SNo: from the Arduino development tool logging window and appended c for the CO2 parts per million (ppm), v for VOC parts per billion (ppb) unique serial number from the ATSHA204A chip. (N.B. pay attention to the case of the field names they are case sensitive)

Azure IoT Central configuration

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

eCO2 and VOC levels in my office for a day

Bill of materials (prices as at August 2019)

  • Seeeduino Nano USD6.90
  • Grove – VOC and eCO2 Gas Sensor (SGP30) USD15.90
  • EasySensors Arduino Nano radio shield RFM95 USD15.00

DF Robot Temperature & Humidity Sensor(SHT20) trial

In preparation for a project to build weather stations to place at a couple of local schools I purchased a DF Robot SHT20 Temperature & Humidity Sensor for evaluation.

Seeeduino Nano, EasySensors Shield & DF Robot Sensor test rig

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 DFRobot Arduino SHT20 library and downloaded a 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 SHT20 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

  DF Robot SHT20 Temperature & Humidity sensor   https://www.dfrobot.com/wiki/index.php/SHT20_I2C_Temperature_%26_Humidity_Sensor_(Waterproof_Probe)_SKU:_SEN0227  

  Seeeduino Nano 
  https://www.seeedstudio.com/Seeeduino-Nano-p-4111.html
  
  Polycarbonate enclosure approx 3.5" x 4.5"
    2 x Cable glands
    1 x ufl to SMA connector
    3M command adhesive strips to hold battery & device in place
   
*/
#include <stdlib.h>
#include <DFRobot_SHT20.h>
#include <LoRa.h>
#include <sha204_library.h>

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

#define UNITS_HUMIDITY "%"
#define UNITS_TEMPERATURE "°c"

// 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] = {""};

// SHT20 Air temperature and humidity sensor
DFRobot_SHT20 sht20;

const byte PayloadSizeMaximum = 64 ;
byte payload[PayloadSizeMaximum];
byte payloadLength = 0 ;


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

#ifdef DEBUG
  while (!Serial);
#endif
 
  Serial.println("Setup called");

  Serial.print("Field gateway:");
  Serial.print(FieldGatewayAddress ) ;
  Serial.print(" Frequency:");
  Serial.print( FieldGatewayFrequency,0 ) ;
  Serial.print("MHz SyncWord:");
  Serial.print( FieldGatewaySyncWord ) ;
  Serial.println();
  
   // Retrieve the serial number then display it nicely
  if(sha204.getSerialNumber(deviceSerialNumber))
  {
    Serial.println("sha204.getSerialNumber failed");
    while (true); // Drop into endless loop requiring restart
  }

  Serial.print("SNo:");
  DisplayHex( deviceSerialNumber, DeviceSerialNumberLength);
  Serial.println();

  Serial.println("LoRa setup start");

  // override the default chip select and reset pins
  LoRa.setPins(ChipSelectPin, ResetPin, InterruptPin);
  if (!LoRa.begin(FieldGatewayFrequency))
  {
    Serial.println("LoRa begin failed");
    while (true); // Drop into endless loop requiring restart
  }

  // Need to do this so field gateway pays attention to messsages from this device
  LoRa.enableCrc();
  LoRa.setSyncWord(FieldGatewaySyncWord);

#ifdef DEBUG_LORA
  LoRa.dumpRegisters(Serial);
#endif
  Serial.println("LoRa Setup done.");

  // Configure the DF Robot SHT20, temperature & humidity sensor
  Serial.println("SHT20 setup start");  
  sht20.initSHT20();
  delay(100);
  sht20.checkSHT20();    
  Serial.println("SHT20 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 ;

  Serial.println("Loop called");

  PayloadReset();  

  humidity = sht20.readHumidity();          
  PayloadAdd( "h", humidity, 0, false);

  temperature = sht20.readTemperature();               
  PayloadAdd( "t", temperature, 1, false);
  
  #ifdef DEBUG_TEMPERATURE_AND_HUMIDITY  
    Serial.print("H:");
    Serial.print( humidity, 0 ) ;
    Serial.print( UNITS_HUMIDITY ) ;
    Serial.print("T:");
    Serial.print( temperature, 1 ) ;
    Serial.println( UNITS_TEMPERATURE ) ;
  #endif

  #ifdef DEBUG_TELEMETRY
    Serial.println();
    Serial.print("RFM9X/SX127X Payload length:");
    Serial.print(payloadLength);
    Serial.println(" bytes");
  #endif

  LoRa.beginPacket();
  LoRa.write(payload, payloadLength);
  LoRa.endPacket();

  Serial.println("Loop done");
  Serial.println();

  delay(SensorUploadDelay - (millis() - currentMilliseconds ));
}


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

  payloadLength = 0 ;

  // prepare the payload header with "To" Address length (top nibble) and "From" address length (bottom nibble)
  
  payload[payloadLength] = (toAddressLength << 4) | fromAddressLength ;
  payloadLength += 1;

  // Copy the "To" address into payload
  memcpy(&payload[payloadLength], to, toAddressLength);
  payloadLength += toAddressLength ;

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


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

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


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

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


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

  memcpy(&payload[payloadLength], sensorId,  sensorIdLength) ;
  payloadLength += sensorIdLength ;
  payload[ payloadLength] = SensorIdValueSeperator;
  payloadLength += 1 ;
  payloadLength += strlen(utoa( value,(char *)&payload[payloadLength],10));
  if (!last)
  {
    payload[ payloadLength] = SensorReadingSeperator;
    payloadLength += 1 ;
  }
  
#ifdef DEBUG_TELEMETRY
  Serial.print("PayloadAdd uint-payloadLength:");
  Serial.print(payloadLength);
  Serial.println( );
#endif
}


void PayloadReset()
{
  byte fromAddressLength = payload[0] & 0xf ;
  byte toAddressLength = payload[0] >> 4 ;
  
  payloadLength = toAddressLength + fromAddressLength + 1;
}


void DisplayHex( byte *byteArray, byte length) 
{
  for (int i = 0; i < length ; i++)
  {
    // Add a leading zero
    if ( byteArray[i] < 16)
    {
      Serial.print("0");
    }
    Serial.print(byteArray[i], HEX);
    if ( i < (length-1)) // Don't put a - after last digit
    {
      Serial.print("-");
    }
  }
}    

The code is available on GitHub.

20:52:09.656 -> Setup called
20:52:09.690 -> Field gateway:LoRaIoT1 Frequency:915000000MHz SyncWord:18
20:52:09.794 -> SNo:01-23-21-61-D6-D1-F5-86-EE
20:52:09.828 -> LoRa setup start
20:52:09.828 -> LoRa Setup done.
20:52:09.862 -> SHT20 setup start
20:52:09.932 -> End of battery: no
20:52:09.932 -> Heater enabled: no
20:52:09.965 -> Disable OTP reload: yes
20:52:09.999 -> SHT20 setup done
20:52:09.999 -> Setup done
20:52:09.999 -> 
20:52:09.999 -> Loop called
20:52:10.067 -> H:60%T:20.0°c
20:52:10.136 -> Loop done
20:52:10.136 -> 
20:53:09.915 -> Loop called
20:53:10.019 -> H:61%T:20.5°c
20:53:10.088 -> Loop done
20:53:10.088 -> 

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)

When I moved the sensor indoors it appeared to take a while to warm up and after a while the metal body still felt cold. The sensor element is surrounded by quite a bit of protective packaging for outdoors use and I that would have a bit more thermal inertia the than the lightweight indoor enclosure.

It would be good to run the sensor alongside a calibrated temperature & humidity sensor to see how accurate and responsive it is.

Bill of materials (prices as at August 2019)

  • Seeeduino Nano USD6.90
  • Grove Screw Terminal USD2.90
  • DF Robot SHT20 I2C Temperature & Humidity Sensor USD22.50
  • EasySensors Arduino Nano radio shield RFM95 USD15.00

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>
#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)

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>
#include <LoRa.h>
#include <sha204_library.h>
#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 & 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 & 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.