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}");
      }
   }
}

Azure Meetup-Budget tank of 91 IoT

The premise of my Azure Meetup presentation was could you build an interesting project on a rainy weekend afternoon with a constrained budget (tank of 91 octane petrol) and minimal soldering .

Budget

Our family car is a VW Passat V6 4Motion which has a 62 Litre tank. The driver usually doesn’t usually stop to fill up until the fuel light has been on for a bit which helped.

PetrolReceipt

Based on the most recent receipt the budget was NZD132.

Where possible I purchased parts locally (the tech equivalent of food miles) or on special.

My bill of materials (prices as at 2018-06) was on budget.

The devDuino V2.2 and nRF24L01 module were USD26.20 approx. NZD37.50 (including freight) from elecrow.

Tradeoffs

I powered my Raspberry PI with a spare cellphone charger (make sure it can supply enough current to reliably power the device).

The devDuino V2.has an ATSHA204A which provides a guaranteed unique 72-bit serial number (makes it harder to screw up provisioning devices in the field).

I use a 32G MicroSD rather than a 16G MicroSD card as I have had issued with 16G cards getting corrupted by more recent upgrades (possibly running out of space?)

The Raspberry PI shield requires a simple modification to enable interrupt driven operation.

My sample devDuino V2.2 client uses an external temperature and humidity sensor, modifying this code to use the onboard temperature sensor an MCP9700 will be covered in another post.

The devDuino V2 is a little bit cheaper USD15.99 NZD37.31, has the same onboard temperature sensor as the V2.2 but no unique serial number chip.

The devDuino V4.0 has an onboard HTU21D temperature + humidity sensor but no unique serial number and the batteries are expensive.

The code and deployment instructions for the nRF24L01 field gateway applications for AdaFruit.IO and Azure IoT Hub/Azure IoT Central are available on hackster.IO.

RPiWithnRF24Plate

AdaFruit.IO has free and USD10.00/month options which work well for many hobbyist projects.

AdaFruitIO

AdaFruit IO basic Netduino HTTP client

I use Netduino devices for teaching and my students often build projects which need a cloud based service like AdaFruit.IO to capture, store and display their sensor data.

My Proof of Concept (PoC) which uses a slightly modified version of the AdaFruit.IO basic desktop HTTP client code has been running on several Netduino 2 Plus, Netduino 3 Ethernet and Netduino 3 Wifi devices for the last couple of days and looks pretty robust.

The Netduino 3 Wifi device also supports https for improved security and privacy. They also make great field gateways as they can run off solar/battery power.

N2PN3WDashBoard

The devices have been uploading temperature and humidity measurements from a Silicon labs Si7005 sensor. (Outside sensor suffering from sunstrike)

N3WifiTemperatureAndHumiditySensor

program.cs

*

Copyright ® 2017 December 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.

http://www.devmobile.co.nz

*/
using System;
using System.Net;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using Microsoft.SPOT.Net.NetworkInformation;
using SecretLabs.NETMF.Hardware.Netduino;
using devMobile.NetMF.Sensor;
using devMobile.IoT.NetMF;

namespace devMobile.IoT.AdaFruitIO.NetMF.Client
{
   public class Program
   {
      private const string adaFruitIOApiBaseUrl = @"https://IO.adafruit.com/api/v2/";
      private const string group = "netduino3";
      private const string temperatureFeedKey = "t";
      private const string humidityFeedKey = "h";
      private const string adaFruitUserName = "YourUserName";
      private const string adaFruitIOApiKey = "YourAPIKey";
      private static readonly TimeSpan timerDueAfter = new TimeSpan(0, 0, 15);
      private static readonly TimeSpan timerPeriod = new TimeSpan(0, 0, 30);
      private static OutputPort led = new OutputPort(Pins.ONBOARD_LED, false);
      private static SiliconLabsSI7005 sensor = new SiliconLabsSI7005();
      private static AdaFruitIoClient adaFruitIoClient = new AdaFruitIoClient(adaFruitUserName, adaFruitIOApiKey, adaFruitIOApiBaseUrl);

      public static void Main()
      {
         // Wait for Network address if DHCP
         NetworkInterface networkInterface = NetworkInterface.GetAllNetworkInterfaces()[0];
         if (networkInterface.IsDhcpEnabled)
         {
            Debug.Print(" Waiting for DHCP IP address");

            while (NetworkInterface.GetAllNetworkInterfaces()[0].IPAddress == IPAddress.Any.ToString())
            {
               Debug.Print(" .");
               led.Write(!led.Read());
               Thread.Sleep(250);
            }
            led.Write(false);
         }

         // Display network config for debugging
         Debug.Print("Network configuration");
         Debug.Print(" Network interface type : " + networkInterface.NetworkInterfaceType.ToString());
         Debug.Print(" MAC Address : " + BytesToHexString(networkInterface.PhysicalAddress));
         Debug.Print(" DHCP enabled : " + networkInterface.IsDhcpEnabled.ToString());
         Debug.Print(" Dynamic DNS enabled : " + networkInterface.IsDynamicDnsEnabled.ToString());
         Debug.Print(" IP Address : " + networkInterface.IPAddress.ToString());
         Debug.Print(" Subnet Mask : " + networkInterface.SubnetMask.ToString());
         Debug.Print(" Gateway : " + networkInterface.GatewayAddress.ToString());

         foreach (string dnsAddress in networkInterface.DnsAddresses)
         {
            Debug.Print(" DNS Server : " + dnsAddress.ToString());
         }

         Timer humidityAndtemperatureUpdates = new Timer(HumidityAndTemperatureTimerProc, null, timerDueAfter, timerPeriod);

         Thread.Sleep(Timeout.Infinite);
      }

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

         try
         {
            double humidity = sensor.Humidity();

            Debug.Print(" Humidity " + humidity.ToString("F0") + "%");
            adaFruitIoClient.FeedUpdate(group, humidityFeedKey, humidity.ToString("F0"));
         }
         catch (Exception ex)
         {
            Debug.Print("Humidifty read+update failed " + ex.Message);

            return;
         }

         try
         {
            double temperature = sensor.Temperature();

            Debug.Print(" Temperature " + temperature.ToString("F1") + "°C");
            adaFruitIoClient.FeedUpdate(group, temperatureFeedKey, temperature.ToString("F1"));
         }
         catch (Exception ex)
         {
            Debug.Print("Temperature read+update failed " + ex.Message);

            return;
         }

         led.Write(false);
      }

      private static string BytesToHexString(byte[] bytes)
      {
         string hexString = string.Empty;

         // Create a character array for hexidecimal conversion.
         const string hexChars = "0123456789ABCDEF";

         // Loop through the bytes.
         for (byte b = 0; b < bytes.Length; b++)          {             if (b > 0)
               hexString += "-";

            // Grab the top 4 bits and append the hex equivalent to the return string.
            hexString += hexChars[bytes[b] >> 4];

            // Mask off the upper 4 bits to get the rest of it.
            hexString += hexChars[bytes[b] & 0x0F];
         }

         return hexString;
      }
   }
}

AdaFruit.IO client.cs, handles feed groups and individual feeds

/*

Copyright ® 2017 December 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.

http://www.devmobile.co.nz

*/
using System;
using System.IO;
using System.Net;
using System.Text;
using Microsoft.SPOT;

namespace devMobile.IoT.NetMF
{
   public class AdaFruitIoClient
   {
      private const string apiBaseUrlDefault = @"http://IO.adafruit.com/api/v2/";
      private string apiBaseUrl = "";
      private string userName = "";
      private string apiKey = "";
      private int httpRequestTimeoutmSec;
      private int httpRequestReadWriteTimeoutmSec;

      public AdaFruitIoClient(string userName, string apiKey, string apiBaseUrl = apiBaseUrlDefault, int httpRequestTimeoutmSec = 2500, int httpRequestReadWriteTimeoutmSec = 5000)
      {
         this.apiBaseUrl = apiBaseUrl;
         this.userName = userName;
         this.apiKey = apiKey;
         this.httpRequestReadWriteTimeoutmSec = httpRequestReadWriteTimeoutmSec;
         this.httpRequestTimeoutmSec = httpRequestTimeoutmSec;
      }

      public void FeedUpdate(string group, string feedKey, string value)
      {
         string feedUrl;

         if (group.Trim() == string.Empty)
         {
            feedUrl = apiBaseUrl + userName + @"/feeds/" + feedKey + @"/data";
         }
         else
         {
            feedUrl = apiBaseUrl + userName + @"/feeds/" + group.Trim() + "." + feedKey + @"/data";
         }

         HttpWebRequest request = (HttpWebRequest)WebRequest.Create(feedUrl);
         {
            string payload = @"{""value"": """ + value + @"""}";
            byte[] buffer = Encoding.UTF8.GetBytes(payload);

            DateTime httpRequestedStartedAtUtc = DateTime.UtcNow;

            request.Method = "POST";
            request.ContentLength = buffer.Length;
            request.ContentType = @"application/json";
            request.Headers.Add("X-AIO-Key", apiKey);
            request.KeepAlive = false;
            request.Timeout = this.httpRequestTimeoutmSec;
            request.ReadWriteTimeout = this.httpRequestReadWriteTimeoutmSec;

            using (Stream stream = request.GetRequestStream())
            {
               stream.Write(buffer, 0, buffer.Length);
            }

            using (var response = (HttpWebResponse)request.GetResponse())
            {
               Debug.Print(" Status: " + response.StatusCode + " : " + response.StatusDescription);
            }

            TimeSpan duration = DateTime.UtcNow - httpRequestedStartedAtUtc;
            Debug.Print(" Duration: " + duration.ToString());
         }
      }
   }
}

Bill of materials for PoC

Mikrobus.Net Quail, Weather & nRF-C clicks and xively

My next proof of concept uses a Weather click and nRF C click to upload temperature and humidity data to a Xively gateway running on a spare Netduino 2 Plus. I have a couple of Azure Event hub gateways (direct & queued) which require a Netduino 3 Wifi (for TLS/AMQPS support) and I’ll build a client for them in a coming post.

I initially purchased an nRF T click but something wasn’t quite right with its interrupt output. The interrupt line wasn’t getting pulled low at all so there were no send success/failure events. If I disabled the pull up resistor and strobed the interrupt pin on start-up the device would work for a while.


using (OutputPort Int = new OutputPort(socket.Int, true))
{
 Int.Write(true);
};

...

_irqPin = new InterruptPort(socket.Int, false, Port.ResistorMode.Disabled, Port.InterruptMode.InterruptEdgeLow);

The code sends a reading every 10 seconds and has been running for a couple of days. It strobes Led1 for each successful send and turns on Led2 when a send fails.

private static readonly byte[] deviceAddress = Encoding.UTF8.GetBytes(&quot;Quail&quot;);
private static readonly byte[] gatewayAddress = Encoding.UTF8.GetBytes(&quot;12345&quot;);
private const byte gatewayChannel = 10;
private const NRFC.DataRate gatewayDataRate = NRFC.DataRate.DR1Mbps;
private const int XivelyUpdateDelay = 10000;
private const char XivelyGatewayChannelIdTemperature = 'J';
private const char XivelyGatewayChannelIdHumidity = 'K';

public static void Main()
{
   NRFC nRF24Click = new NRFC(Hardware.SocketFour);
   nRF24Click.Configure(deviceAddress, gatewayChannel, gatewayDataRate);
   nRF24Click.OnTransmitFailed += nRF24Click_OnTransmitFailed;
   nRF24Click.OnTransmitSuccess += nRF24Click_OnTransmitSuccess;
   nRF24Click.Enable();

   // Configure the weather click
   WeatherClick weatherClick = new WeatherClick(Hardware.SocketOne, WeatherClick.I2CAddresses.Address0);
   weatherClick.SetRecommendedMode(WeatherClick.RecommendedModes.WeatherMonitoring);

   Thread.Sleep(XivelyUpdateDelay);

   while (true)
   {
      string temperatureMessage = XivelyGatewayChannelIdTemperature + weatherClick.ReadTemperature().ToString("F1");
      Debug.Print(temperatureMessage);
      MBN.Hardware.Led1.Write(true);
      nRF24Click.SendTo(gatewayAddress, Encoding.UTF8.GetBytes(temperatureMessage));

      Thread.Sleep(XivelyUpdateDelay);

      string humidityMessage = XivelyGatewayChannelIdHumidity + weatherClick.ReadHumidity().ToString("F1");
      Debug.Print(humidityMessage);
      MBN.Hardware.Led1.Write(true);
      nRF24Click.SendTo(gatewayAddress, Encoding.UTF8.GetBytes(humidityMessage));

      Thread.Sleep(XivelyUpdateDelay);
   }
}

static void nRF24Click_OnTransmitSuccess()
{
   MBN.Hardware.Led1.Write(false);
   if (MBN.Hardware.Led2.Read())
   {
      MBN.Hardware.Led2.Write(false);
   }

   Debug.Print("nRF24Click_OnTransmitSuccess");
}

static void nRF24Click_OnTransmitFailed()
{
   MBN.Hardware.Led2.Write(true);

   Debug.Print("nRF24Click_OnTransmitFailed");
}

I need to have a look at interfacing some more sensors and soak testing the solution.

The MikroBus.Net team have done a great job with the number & quality of the drivers they have available.

Fez Lemur & Panda III AnalogInput read rates

I had previously have measured the AnalogInput read rate of my Netduino devices and was surprised by some of the numbers. Now, I have another project in the planning phase which will be using a GHI Electronics Fez Lemur or Fez Panda III device and had time for a quick test.

This is just a simple test, not terribly representative of real world just to get comparable numbers.

public static void Main()
{
   int value;
   AnalogInput x1 = new AnalogInput(FEZLemur.AnalogInput.A0);
   Stopwatch stopwatch = Stopwatch.StartNew();

   Debug.Print("Starting");

   stopwatch.Start();
   for (int i = 0; i < SampleCount; i++)
   {
      value = x1.ReadRaw();
   }
   stopwatch.Stop();

   Debug.Print("Duration = " + stopwatch.ElapsedMilliseconds.ToString() + " mSec " + (SampleCount * 1000 / stopwatch.ElapsedMilliseconds).ToString() + "/sec");
}

Fez Lemur 84 MHz CPU
Duration = 2855 mSec 35026/sec
Duration = 2854 mSec 35038/sec
Duration = 2854 mSec 35038/sec
Duration = 2854 mSec 35038/sec
Duration = 2861 mSec 34952/sec

Duration = 2856 mSec 35014/sec
Duration = 2854 mSec 35038/sec
Duration = 2855 mSec 35026/sec
Duration = 2854 mSec 35038/sec
Duration = 2854 mSec 35038/sec

Fez Panda III 180MHz CPU
Duration = 1799 mSec 55586/sec
Duration = 1799 mSec 55586/sec
Duration = 1799 mSec 55586/sec
Duration = 1799 mSec 55586/sec
Duration = 1799 mSec 55586/sec

Duration = 1799 mSec 55586/sec
Duration = 1799 mSec 55586/sec
Duration = 1799 mSec 55586/sec
Duration = 1799 mSec 55586/sec
Duration = 1799 mSec 55586/sec

It looks like the GHI Team have a performant implementation of AnalogInput.ReadRaw()

Mikrobus.Net Quail, EthClick and xively

My second proof of concept application for the Mikrobus.Net Quail and EthClick uploads temperature and humidity data to Xively every 30 seconds for display and analysis.

Temperature and humidity Xively data stream

Temperature and humidity Xively data stream

The Xively REST API uses an HTTP PUT which initially didn’t work because the payload was not getting attached.

I patched the AssembleRequest method in the EtherClick driver to fix this issue.

private byte[] AssembleRequest()
{
   var a = RequestType;
   a += " " + Path + " " + Protocol + "\r\nHost: ";
   a += Host + "\r\n";

   foreach (object aHeader in Headers.Keys)
      a += (string)aHeader + ": " + (string)Headers[aHeader] + "\r\n";

   a += "\r\n"; // Cache-Control: no-cache\r\n  //Accept-Charset: utf-8;\r\n

   if (Content != null && Content != string.Empty && (RequestType == "POST" || RequestType == "PUT")) a += Content;

   return Encoding.UTF8.GetBytes(a);
}

The code reads the WeatherClick temperature and humidity values then assembles a CSV payload which it uploads with an HTTP PUT

</pre>
public class Program
{
   private const string xivelyHost = @"api.xively.com";
   private const string xivelyApiKey = @"YourAPIKey";
   private const string xivelyFeedId = @"YourFeedID";

   public static void Main()
   {
      WeatherClick weatherClick = new WeatherClick(Hardware.SocketOne, WeatherClick.I2CAddresses.Address0);
      weatherClick.SetRecommendedMode(WeatherClick.RecommendedModes.WeatherMonitoring);

      EthClick ethClick = new EthClick(Hardware.SocketTwo);
      ethClick.Start(ethClick.GenerateUniqueMacAddress("devMobileSoftware"), "QuailDevice");

      // Wait for an internet connection
      while (true)
      {
         if (ethClick.ConnectedToInternet)
         {
            Debug.Print("Connected to Internet");
            break;
         }
         Debug.Print("Waiting on Internet connection");
      }

      while (true)
      {
         Debug.Print("T " + weatherClick.ReadTemperature().ToString("F1") + " H " + weatherClick.ReadHumidity().ToString("F1") + " P " + weatherClick.ReadPressure(PressureCompensationModes.Uncompensated).ToString("F1"));

         HttpRequest request = new HttpRequest(@"http://" + xivelyHost + @"/v2/feeds/" + xivelyFeedId + @".csv");
         request.Host = xivelyHost;
         request.RequestType = "PUT";
         request.Headers.Add("Content-Type", "text/csv");
         request.Headers.Add("X-ApiKey", xivelyApiKey );

         request.Content = "OfficeT," + weatherClick.ReadTemperature().ToString("F1") + "\r\n" + "OfficeH," + weatherClick.ReadHumidity().ToString("F1") ;
         request.Headers.Add("Content-Length", request.Content.Length.ToString());

         var response = request.Send();
         if (response != null)
         {
            Debug.Print("Response: " + response.Message);
         }
         else
         {
            Debug.Print("No response");
         }
      Thread.Sleep(30000);
      }
   }
}

MikrobustNet Quail with Eth and Weather Clicks

MikrobustNet Quail with Eth and Weather Clicks

This proof of concept code appears to be reliable and has run for days at a time. The IP stack looks like it needs a bit more work.