Wireless field gateway protocol V2

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

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

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

0 = Echo

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

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

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

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

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

 

Wireless field gateway protocol V1

I’m going to build a number of nRF2L01P field gateways (Netduino Ethernet & Wifi running .NetMF, Raspberry PI running Windows 10 IoT Core, RedBearLab 3200  etc.), clients which run on a variety of hardware (Arduino, devDuino, Netduino, Seeeduino etc.) which, then upload data to a selection of IoT Cloud services (AdaFruit.IO, ThingSpeak, Microsoft IoT Central etc.)

The nRF24L01P is widely supported with messages up to 32 bytes long, low power consumption and 250kbps, 1Mbps and 2Mbps data rates.

The aim is to keep the protocol simple (telemetry only initially) to implement and debug as the client side code will be utilised by high school student projects.

The first byte of the message specifies the message type

0 = Echo

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

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

[0] – Set to 1

[1] – Device identifier length

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

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

Overtime I will support more message types and wireless protocols.

 

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

Xively Personal is being retired

This is going to cause me a problem especially my Netduino based nRF24 Xively Field gateway which gets used in quite a few of my student projects. I’m looking for a replacement Internet of Things service which has http/s and/or mqtt, amqp support, C & C#  client libraries (which I can get to work on Windows 10 IoT Core & NetMF) would be a bonus.

From the Xively email

”After careful consideration, LogMeIn has made the decision to retire Xively Personal from its current line of products effective January 15, 2018 at 12:00PM ET . Please note that LogMeIn will continue to offer our Xively Enterprise edition – there is no change to that edition and we will continue to support that platform as part of our IoT business.

Retiring a product is never an easy decision, and we recognize it does introduce potential challenges to active users. So we want to make sure you have all the information you need to make as seamless a transition as possible.

Access to your account:
Your Xively Personal account will remain active until January 15th. Please note that devices will not be accessible via the Xively Personal service once it is retired.

Transferring your products to another IoT service:
Should you choose to switch to another service, there are essentially two options.

1) Migrate to Xively Enterprise: The latest Enterprise version of Xively is built on a more modern and reliable architecture, which brings the benefits of pre-built hardware integrations, identity and device management features, MQTT messaging, and best-in-class security, but it may require some reconfiguring of your current devices. We do offer a 30 day free trial of Xively Enterprise should you want to try it out for yourself.

2) Migrate to another free service: If your use is primarily for experimenting and personal projects, there are several free IoT platform options on the market, such as Adafruit, Thingspeak, or SparkFun.”

One of the suggestions – Sparkfun Phant has been retired

Some possible alternatives in no particular order (this list may grow)

AdaFruit.IO – The internet of things for everyone

Microsoft IoT Central – Enterprise-grade IoT SaaS

ThingSpeak – The open IoT platform with MATLAB analytics

Blynk – Democratizing the Internet of Things

Cayenne – Simplify the Connected World

Thinger.io platform

SenseIoT – Internet of Things Data Hosting Platform

Temboo – Tools for Digital Transformation

Carriots by Altair

Nearbus – An IoT Open Project

ubidots – An application Builder for the Internet of Things

Microsoft IoT Central – Enterprise Grade IoT SaaS

Kii Cloud

Artik – End-to-end IoT Platform

goplusplatform – Connect your things with GO+

I’m initially looking for a platform which is the “least painful” transition from Xively.

nRF24 Windows 10 IoT Core Background Task

First step is to build a basic Windows 10 IoT Core background task which can receive and display messages sent from a variety of devices across an nRF24L01 wireless link.

If you create a new “Windows IoT Core” “Background Application” project then copy this code into StartupTasks.cs the namespace has to be changed in the C# file, project properties\library\Default namespace and “Package.appxmanifest”\declarations\Entry Point.

/*

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.Diagnostics;
using System.Text;
using Radios.RF24;
using Windows.ApplicationModel.Background;

namespace devmobile.IoTCore.nRF24BackgroundTask
{
    public sealed class StartupTask : IBackgroundTask
    {
      private const byte ChipEnablePin = 25;
      private const byte ChipSelectPin = 0;
      private const byte nRF24InterruptPin = 17;
      private const string BaseStationAddress = "Base1";
      private const byte nRF24Channel = 10;
      private RF24 Radio = new RF24();
      private BackgroundTaskDeferral deferral;

      public void Run(IBackgroundTaskInstance taskInstance)
        {
         Radio.OnDataReceived += Radio_OnDataReceived;
         Radio.OnTransmitFailed += Radio_OnTransmitFailed;
         Radio.OnTransmitSuccess += Radio_OnTransmitSuccess;

         Radio.Initialize(ChipEnablePin, ChipSelectPin, nRF24InterruptPin);
         Radio.Address = Encoding.UTF8.GetBytes(BaseStationAddress);
         Radio.Channel = nRF24Channel;
         Radio.PowerLevel = PowerLevel.High;
         Radio.DataRate = DataRate.DR250Kbps;
         Radio.IsEnabled = true;

         Debug.WriteLine("Address: " + Encoding.UTF8.GetString(Radio.Address));
         Debug.WriteLine("PA: " + Radio.PowerLevel);
         Debug.WriteLine("IsAutoAcknowledge: " + Radio.IsAutoAcknowledge);
         Debug.WriteLine("Channel: " + Radio.Channel);
         Debug.WriteLine("DataRate: " + Radio.DataRate);
         Debug.WriteLine("IsDynamicAcknowledge: " + Radio.IsDyanmicAcknowledge);
         Debug.WriteLine("IsDynamicPayload: " + Radio.IsDynamicPayload);
         Debug.WriteLine("IsEnabled: " + Radio.IsEnabled);
         Debug.WriteLine("Frequency: " + Radio.Frequency);
         Debug.WriteLine("IsInitialized: " + Radio.IsInitialized);
         Debug.WriteLine("IsPowered: " + Radio.IsPowered);

         deferral = taskInstance.GetDeferral();

         Debug.WriteLine("Run completed");
      }

      private void Radio_OnDataReceived(byte[] data)
      {
         // Display as Unicode
         string unicodeText = Encoding.UTF8.GetString(data);
         Debug.WriteLine("Unicode - Payload Length {0} Unicode Length {1} Unicode text {2}", data.Length, unicodeText.Length, unicodeText);

         // display as hex
         Debug.WriteLine("Hex - Length {0} Payload {1}", data.Length, BitConverter.ToString(data));
      }

      private void Radio_OnTransmitSuccess()
      {
         Debug.WriteLine("Transmit Succeeded!");
      }

      private void Radio_OnTransmitFailed()
      {
         Debug.WriteLine("Transmit Failed!");
      }
   }
}

This was displayed in the output window of Visual Studio

Address: Base1
PA: 15
IsAutoAcknowledge: True
Channel: 10
DataRate: DR250Kbps
IsDynamicAcknowledge: False
IsDynamicPayload: True
IsEnabled: True
Frequency: 2410
IsInitialized: True
IsPowered: True
Run completed

Interrupt Triggered: FallingEdge
Unicode – Payload Length 19 Unicode Length 19 Unicode text T  23.8,H  73,V 3.26
Hex – Length 19 Payload 54-20-32-33-2E-38-2C-48-20-20-37-33-2C-56-20-33-2E-32-36
Interrupt Triggered: RisingEdge

Note the odd formatting of the Temperature and humidity values which is due to the way dtostrf function in the Atmel AVR library works.

Also noticed the techfooninja nRF24 library has configurable output power level which I will try to retrofit onto the Gralin NetMF library.

Next, several simple Arduino, devDuino V2.2, Seeeduino V4.2 and Netduino 2/3 clients (plus possibly some others)

nRF24 Windows 10 IoT Core reboot

My first live deployment of the nRF24L01 Windows 10 IoT Core field gateway is now scheduled for mid Q1 2018 so time for a reboot. After digging out my Raspbery PI 2/3 devices and the nRF24L01+ shield (with modifications detailed here) I have a basic plan with some milestones.

My aim is to be able to wirelessly acquire data from several dozen Arduino, devduino, seeeduino, and Netduino devices, Then, using a field gateway on a Raspberry PI running Windows 10 IoT Core upload it to Microsoft IoT Central

First bit of code – Bleepy a simple background application to test the piezo beeper on the RPI NRF24 Shield

namespace devmobile.IoTCore.Bleepy
{
   public sealed class StartupTask : IBackgroundTask
   {
      private BackgroundTaskDeferral deferral;
      private const int ledPinNumber = 4;
      private GpioPin ledGpioPin;
      private ThreadPoolTimer timer;

      public void Run(IBackgroundTaskInstance taskInstance)
      {
         var gpioController = GpioController.GetDefault();
         if (gpioController == null)
         {
            Debug.WriteLine("GpioController.GetDefault failed");
            return;
         }

         ledGpioPin = gpioController.OpenPin(ledPinNumber);
         if (ledGpioPin == null)
         {
            Debug.WriteLine("gpioController.OpenPin failed");
            return;
         }

         ledGpioPin.SetDriveMode(GpioPinDriveMode.Output);

         this.timer = ThreadPoolTimer.CreatePeriodicTimer(Timer_Tick, TimeSpan.FromMilliseconds(500));

         deferral = taskInstance.GetDeferral();

         Debug.WriteLine("Rum completed");
      }

      private void Timer_Tick(ThreadPoolTimer timer)
      {
         GpioPinValue currentPinValue = ledGpioPin.Read();

         if (currentPinValue == GpioPinValue.High)
         {
            ledGpioPin.Write(GpioPinValue.Low);
         }
         else
         {
            ledGpioPin.Write(GpioPinValue.High);
         }
      }
   }
}

Note the blob of blu tack over the piezo beeper to mute noise
nRF24ShieldMuted

nRF24 Windows 10 IoT Core Hardware

Taking my own advice I decided to purchase a couple of Raspberry Pi to NRF24L01 shields from Ceech a vendor on Tindie.

The nRF24L01 libraries for my .Net Micro framework and WIndows 10 IoT Core devices use an interrupt driver approach rather than polling status registers to see what is going on.

Like most Raspberry PI shields intended to be used with a *nix based operating system the interrupt pin was not connected to a General Purpose Input/Output (GPIO) pin.

NRF24PiPlateModification

My first step was to add a jumper wire from the pin 8 on the nRF24L01 to GPIO pin 17 on Raspberry PI connector.

I then downloaded the techfooninja Radios.RF24 library for Windows IoT core and update the configuration to suit my modifcations. In the TestApp the modifications were limited to changing the interrupt pin from GPI 4 to GPO 17

private const byte IRQ_PIN = 4;

private const byte IRQ_PIN = 17;

I used a socket for the nRF24L01 device so I can trial different devices, for a production system I would solder the device to the shield to improve reliability.

RPiWithnRF24Plate

I then ran the my test application software in a stress test rig overnight to check for any reliability issues. The 5 x netduino devices were sending messages every 500mSec

RPIStressTester