nRF24L01 Raspberry PI Gateway Hardware

For those who came to my MS Ignite AU Intelligent Cloud booth session

Building Wireless Field Gateways

Connecting wireless sensor nodes to the cloud is not the mission it used to be, because the Azure team (and many OS projects) have developed tooling which can help hobbyist and professional developers build solutions. How could you build a home scale robust, reliable and secure solution with off the shelf kit without blowing the budget?

Sparkfun nRF24L01 module &Adafruit perma proto hat

NRF24L01 Raspberry PI DIY Gateway Hardware

BoM (all prices as at Feb 2016)

You will also need some short lengths of wire and a soldering iron.

For those who want an “off the shelf” solution (still requires a minor modification for interrupt support) I have used the Raspberry Pi to NRF24l01+ Shield USD9.90

2015-09-25t072754-447z-20150925_091942-855x570_q85_pad_rcrop

Instructions for modifications and software to follow.

Mikrobus.Net Quail Robot

In a previous post I had replaced a Netduino and Elecfreaks Joystick shield based remote control with a MikrobusNet Quail, thumbstick click and an nRF-C click. The next step was to replace the Netduino on the robot chassis with a MikrobusNet Quail, a pair of DC Motor Clicks and an nRF-C click.

Bill of materials (prices in USD as at Feb 2016)

The first version of the robot uses a pair of battery packs one for the motors the other for the Quail board.

MikrobusNetQual4WDRobot

The drivers developed by MikroBUSNet team greatly reduced the amount of code I had to write to get the robot to work.

public class Program
{
   private static double Scale = 100.0;
   private static byte RobotControlChannel = 10;
   private static byte[] ControllerAddress = Encoding.UTF8.GetBytes("RC1");
   private static byte[] RobotAddress = Encoding.UTF8.GetBytes("RB1");
   private static TimeSpan MessageMaximumInterval = new TimeSpan(0, 0, 1);
   private static DateTime _MessageLastReceivedAt = DateTime.UtcNow;
   private static DCMotorClick motor1 = new DCMotorClick(Hardware.SocketOne);
   private static DCMotorClick motor2 = new DCMotorClick(Hardware.SocketTwo);

public static void Main()
{
   NRFC nrf = new NRFC(Hardware.SocketFour);
   nrf.Configure(RobotAddress, RobotControlChannel);
   nrf.OnTransmitFailed += nrf_OnTransmitFailed;
   nrf.OnTransmitSuccess += nrf_OnTransmitSuccess;
   nrf.OnDataReceived += nrf_OnDataReceived;
   nrf.Enable();

   Timer CommunicationsMonitorTimer = new Timer(CommunicationsMonitorTimerProc, null, 500, 500);

   Thread.Sleep(Timeout.Infinite);
}

static void nrf_OnDataReceived(byte[] data)
{
   Hardware.Led1.Write(true);
   _MessageLastReceivedAt = DateTime.UtcNow;

   if (data.Length != 5)
   {
   return;
   }

   Debug.Print("M1D=" + data[0].ToString() + " M2D=" + data[1].ToString() + " M1S=" + data[2].ToString() + " M2S=" + data[3].ToString());
   if (data[0] == 1)
   {
      motor1.Move(DCMotorClick.Directions.Forward, (data[2] / Scale ));
   }
   else
   {
     motor1.Move(DCMotorClick.Directions.Backward, (data[2] / Scale ));
   }

   if (data[1] == 1)
   {
      motor2.Move(DCMotorClick.Directions.Forward, (data[3] / Scale ));
   }
   else
   {
      motor2.Move(DCMotorClick.Directions.Backward, (data[3] / Scale ));
   }
}

private static void CommunicationsMonitorTimerProc(object status)
{
   if ((DateTime.UtcNow - _MessageLastReceivedAt) > MessageMaximumInterval)
   {
      Debug.Print("Communications timeout");

      motor1.Move(MBN.Modules.DCMotorClick.Directions.Forward, 0.0);
      motor2.Move(MBN.Modules.DCMotorClick.Directions.Forward, 0.0);
   }
}

I have kept the communications monitoring functionality which stops the motors when the robot gets out of range of the remote control software fails.

 

 

 

Mikrobus.Net Quail Robot Remote Control

In a previous pair of posts  (part1 & part2) in February 2014 I built a 4WD Robot and remote control using a pair of Netduinos, an elecfreaks Smart Car Chassis 4WD, an elecfreaks joystick 2.4, an Embedded coolness nRF24Lo1 shield and a Pololu MC33926 motor shield.

My Quail device looked like a good platform for building a handheld control with a different form factor.

Bill of materials (prices in USD as at Jan 2016)

Quail4WDRobotController

The Quail device and battery pack aren’t quite small enough to work with one hand. A Mikrobus.Net Dalmatian or Tuatara based remote might be easier to use.

I tried using the thumbstick button pushed message for the horn functionality but it made the throttle and heading jump.

The first version of the code is just to test the wireless link, the motor speed code needs a little work.(Currently the device won’t rotate with motors going in opposite directions)

public class Program
{
   private const double Deadband = 0.1;
   private static double Scale = 100.0;
   private static byte RobotControlChannel = 10;
   private static byte[] ControllerAddress = Encoding.UTF8.GetBytes("RC1");
   private static byte[] RobotAddress = Encoding.UTF8.GetBytes("RB1");

   public static void Main()
   {
      ThumbstickClick thumbStick = new ThumbstickClick(Hardware.SocketThree);
      thumbStick.ThumbstickOrientation = ThumbstickClick.Orientation.RotateZeroDegrees;
      thumbStick.Calibrate();

      NRFC nrf = new NRFC(Hardware.SocketFour);
      nrf.Configure(ControllerAddress, RobotControlChannel );
      nrf.OnTransmitFailed += nrf_OnTransmitFailed;
      nrf.OnTransmitSuccess += nrf_OnTransmitSuccess;
      nrf.Enable();

      while (true)
      {
         byte motor1Direction, motor2Direction;
         byte motor1Speed, motor2Speed;
         double x = thumbStick.GetPosition().X;
         double y = thumbStick.GetPosition().Y;

         Debug.Print("X=: + x.ToString("F1") + " Y=" + y.ToString("F1") + " IsPressed=" + thumbStick.IsPressed);

         // See if joystick x or y is in centre deadband
         if (System.Math.Abs(x) < Deadband)
         {
            x = 0.0;
         }

         // See if joystick y is in centre deadband
         if (System.Math.Abs(y) < Deadband)
         {
            y = 0.0;
         }

         // Set direction of both motors, no swivel on spot yet
         if (y >= 0.0)
         {
            motor1Direction = (byte)1;
            motor2Direction = (byte)1;
         }
         else
         {
            motor1Direction = (byte)0;
            motor2Direction = (byte)0;
         }

         // Straight ahead/backward
         if (x == 0.0)
         {
            motor1Speed = (byte)(System.Math.Abs(y) * Scale);
            motor2Speed = (byte)(System.Math.Abs(y) * Scale);
         }
         // Turning right
         else if (x > 0.0)
         {
            motor1Speed = (byte)(System.Math.Abs(y) * Scale);
            motor2Speed = (byte)(System.Math.Abs(y) * (1.0 - System.Math.Abs(x)) * Scale);
         }
         // Turning left
         else
         {
            motor1Speed = (byte)(System.Math.Abs(y) * (1.0 - System.Math.Abs(x)) * Scale);
            motor2Speed = (byte)(System.Math.Abs(y) * Scale);
         }

         Debug.Print("X=" + x.ToString("F1") + " Y=" + y.ToString("F1") + " IsPressed=" + thumbStick.IsPressed + " M1D=" + motor1Direction.ToString() + " M2D=" + motor2Direction.ToString() + " M1S=" + motor1Speed.ToString() + " M2S=" + motor2Speed.ToString());

         byte[] command =
         {
            motor1Direction,
            motor2Direction,
            motor1Speed,
            motor2Speed,
            (byte)0)
         };
         nrf.SendTo(RobotAddress, command );

         MBN.Hardware.Led1.Write(true);

         Thread.Sleep(250);
      }
   }

   static void nrf_OnTransmitSuccess()
   {
     MBN.Hardware.Led1.Write(false);
     Debug.Print("nrf_OnTransmitSuccess");
   }

   static void nrf_OnTransmitFailed()
   {
      Debug.Print("nrf_OnTransmitFailed");
   }
}

The Mikrobus.Net team have done a great job with the number and quality of the drivers for the Mikroe click boards. The Mikroe click boards are individually packaged with professionally written click specific and handling instructions.

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.

Netduino 3 Wifi Queued Azure Event Hub Field Gateway V1.0

My ADSL connection had been a bit flaky which had meant I had lost some sensor data with my initial Azure Event Hub gateway. In attempt make the solution more robust this version of the gateway queues unsent messages using the on-board MicroSD card support.

The code assumes that a file move is an “atomic operation”, so it streams the events received from the devices into a temporary directory (configurable) then moves them to the upload directory (configurable).

This code is proof of concept and needs to be soak tested, improved error handling and some additional multi threading locking added plus the magic constants refactored.

This code is called in the nRF24 receive messages handler

private void OnReceive(byte[] data)
{
   activityLed.Write(!activityLed.Read());

   // Ensure that we have a payload
   if (data.Length < 1 )
   {
      Debug.Print( "ERROR - Message has no payload" ) ;
      return ;
   }

   string message = new String(Encoding.UTF8.GetChars(data));
   Debug.Print("+" + DateTime.UtcNow.ToString("HH:mm:ss") + " L=" + data.Length + " M=" + message);

   string filename = DateTime.UtcNow.ToString("yyyyMMddhhmmssff") + ".txt";

   string tempDirectory = Path.Combine("\\sd", "temp");
   string tempFilePath = Path.Combine(tempDirectory, filename);

   string queueDirectory = Path.Combine("\\sd", "data");
   string queueFilePath = Path.Combine(queueDirectory, filename);

   File.WriteAllBytes(tempFilePath, data);

   File.Move(tempFilePath, queueFilePath);

   new Microsoft.SPOT.IO.VolumeInfo("\\sd").FlushAll();
}

A timer initiates the upload process which uses the AMQPNetlite library

bool UploadInProgress = false;

      
void uploaderCallback(object state)
{
   Debug.Print("uploaderCallback - start");

   if (UploadInProgress)
   {
      return;
   }
   UploadInProgress = true;

   string[] eventFilesToSend = Directory.GetFiles(Path.Combine("\\sd", "data")) ;

   if ( eventFilesToSend.Length == 0 )
   {
      Debug.Print("uploaderCallback - no files");
      UploadInProgress = false;
      return ;
   }

   try
   {
      Debug.Print("uploaderCallback - Connect");
      Connection connection = new Connection(new Address(serviceBusHost, serviceBusPort, serviceBusSasKeyName, serviceBusSasKey));

      Session session = new Session(connection);

      SenderLink sender = new SenderLink(session, "send-link", eventHubName);

      for (int index = 0; index < System.Math.Min(eventUploadBatchSizeMaximum, eventFilesToSend.Length); index++)
      {
         string eventFile = eventFilesToSend[ index ] ;

         Debug.Print("-" + DateTime.UtcNow.ToString("HH:mm:ss") + " " + eventFile ); ;

         Message message = new Message()
         {
            BodySection = new Data()
            {
               Binary = File.ReadAllBytes(eventFile),
            },
         ApplicationProperties = new Amqp.Framing.ApplicationProperties(),
         };

         FileInfo fileInfo = new FileInfo(eventFile);

         message.ApplicationProperties["AcquiredAtUtc"] = fileInfo.CreationTimeUtc;
         message.ApplicationProperties["UploadedAtUtc"] = DateTime.UtcNow;
         message.ApplicationProperties["GatewayId"] = gatewayId;
         message.ApplicationProperties["DeviceId"] = deviceId;
         message.ApplicationProperties["EventId"] = Guid.NewGuid();

         sender.Send(message);

         File.Delete(eventFile);

         new Microsoft.SPOT.IO.VolumeInfo("\\sd").FlushAll();
      }

      sender.Close();
      session.Close();
      connection.Close();
   }
   catch (Exception ex)
   {
      Debug.Print("ERROR: Upload failed with error: " + ex.Message);
   }
   finally
   {
      Debug.Print("uploaderCallback - finally");
      UploadInProgress = false;
   }
}

The timer period and number of files uploaded in each batch is configurable. I need to test the application to see how it handles power outages and MicroSD card corruption. The source is Netduino NRF24L01 AMQPNetLite Queued Azure EventHub Gatewaywith all the usual caveats.

This project wouldn’t have been possible without

Netduino 3 Wifi Azure Event Hub Field Gateway V1.0

The Netduino 3 Wifi device supports TLS connectivity and looked like it could provide a low power consumption field gateway to an Azure EventHub for my nRF24L01 equipped Netduino, Arduino & devDuino 1.3, 2.0 & 3.0 devices.

Netduino 3 Wifi Azure Event Hub Field Gateway

Netduino 3 Wifi Azure Field Gateway and a selection of arduino & devDuino devices

Bill of materials for field gateway prices as at (Sept 2015)

  • Netduino 3 Wifi USD69.95
  • SeeedStudio Solar Shield USD13.95
  • Lithium Ion 3000mAH battery USD15.00
  • Embedded coolness nRF24L01 shield with high power module USD17.85

The software uses AMQPNetLite which provides a lightweight implementation of the AMQP protocol (on the .Net framework, .Net Compact Framework, .Net Micro Framework, and WindowsPhone platforms) and the Nordic nRF24L01 Net Micro Framework Driver.The first version of the software is a proof of concept and over time I will add functionality and improve the reliability.

On application start up the nRF24L01, Azure Event Hub and network settings are loaded from the built in MicroSD card.

// Write empty template of the configuration settings to the SD card if pin D0 is high
if (!File.Exists(Path.Combine("\\sd", "app.config")))
{
   Debug.Print("Writing template configuration file then stopping");

   ConfigurationFileGenerate();

   Thread.Sleep(Timeout.Infinite);
}
appSettings.Load();

If there is no configuration file on the MicroSD card an empty template is created.

private void ConfigurationFileGenerate()
{
   // Write empty configuration file
   appSettings.SetString(nRF2L01AddressSetting, "Base1");
   appSettings.SetString(nRF2L01ChannelSetting, "10");
   appSettings.SetString(nRF2L01DataRateSetting, "0");

   appSettings.SetString(serviceBusHostSetting, "serviceBusHost");
   appSettings.SetString(serviceBusPortSetting, "5671");
   appSettings.SetString(serviceBusSasKeyNameSetting, "serviceBusSasKeyName");
   appSettings.SetString(serviceBusSasKeySetting, "serviceBusSasKey");
   appSettings.SetString(eventHubNameSetting, "eventHubName");

   appSettings.Save();
}

Once the Wifi connection has been established the device connects to a specified NTP server so any messages have an accurate timestamp and then initiates an AMQP connection.

Debug.Print("Network time");
try
{
   DateTime networkTime = NtpClient.GetNetworkTime(ntpServerHostname);
   Microsoft.SPOT.Hardware.Utility.SetLocalTime(networkTime);
   Debug.Print(networkTime.ToString(" dd-MM-yy HH:mm:ss"));
}
catch (Exception ex)
{
   Debug.Print("ERROR: NtpClient.GetNetworkTime: " + ex.Message);
   Thread.Sleep(Timeout.Infinite);
}
Debug.Print("Network time done");

// Connect to AMQP gateway
Debug.Print("AMQP Establish connection");
try
{
   Address address = new Address(serviceBusHost, serviceBusPort, serviceBusSasKeyName, serviceBusSasKey);
   connection = new Connection(address);
}
catch (Exception ex)
{
   Debug.Print("ERROR: AMQP Establish connection: " + ex.Message);
   Thread.Sleep(Timeout.Infinite);
}
Debug.Print("AMQP Establish connection done");

After the device has network connectivity, downloaded the correct time and connected to AMQP hub the nRF241L01 device is initialised.

The first version of the software starts a new thread to handle each message and handles connectivity failures badly. These issues and features like local queuing of messages will be added in future iterations.

private void OnReceive(byte[] data)
{
   activityLed.Write(!activityLed.Read());

   // Ensure that we have a payload
   if (data.Length < 1 ) { Debug.Print( "ERROR - Message has no payload" ) ; return ; } string message = new String(Encoding.UTF8.GetChars(data)); Debug.Print(DateTime.UtcNow.ToString("HH:mm:ss") + " " + gatewayId + " " + data.Length + " " + message); Thread thread = new Thread(() => EventHubSendMessage(connection, eventHubName, deviceId, gatewayId, data));
   thread.Start();
}



private void EventHubSendMessage(Connection connection, string eventHubName, string deviceId, string gatewayId, byte[] messageBody)
{
   try
   {
      Session session = new Session(connection);
      SenderLink sender = new SenderLink(session, "send-link", eventHubName);

      Message message = new Message()
      {
         BodySection = new Data()
         {
            Binary = messageBody
         },
         ApplicationProperties = new Amqp.Framing.ApplicationProperties(),
      };

      message.ApplicationProperties["UploadedAtUtc"] = DateTime.UtcNow;
      message.ApplicationProperties["GatewayId"] = gatewayId;
      message.ApplicationProperties["DeviceId"] = deviceId;
      message.ApplicationProperties["EventId"] = Guid.NewGuid().ToString();

      sender.Send(message);

      sender.Close();
      session.Close();
      }
   catch (Exception ex)
   {
      Debug.Print("ERROR: Publish failed with error: " + ex.Message);
   }
}

Initially the devices send events with a JSON payload.

ServiceBus Explorer

JSON Event messages displayed in ServiceBus Explorer

The code is available NetduinoNRF24L01AMQPNetLiteAzureEventHubGatewayV1.0 and when I have a spare afternoon I will upload to github.

Netduino 3 Wifi xively nRF24L01 Gateway

The first version of this code acquired data from a number of *duino devices and uploaded it to xively for a week without any problems(bar my ADSL modem dropping out every so often which it recovered from without human intervention). The data streams are the temperature and humidity for the three bedrooms in my house (the most reliable stream is Bedroom 1). Next version will use the new Netduino.IP stack and run on a Netduino 2 Plus

Netduino 3 Wifi with nRF24L01 shield

Netduino 3 Wifi + nRF24L01 shield

To make the software easy to setup all the gateway configuration is stored on a MicroSD and can be modified with a text editor. When the application starts it looks for a file in the root directory of the MicroSD card called app.config. If the file does not exist an empty template is created.

httprequestreadwritetimeoutmsec=2500
httprequesttimeoutmsec=2500
webproxyaddress=
webproxyport=
xivelyapibaseurl=http://api.xively.com/v2/feeds/
xivelyapikey=XivelyAPIKeyGoesHere
xivelyapifeedid=XivelyFeedIDGoesHere
xivelyapicontenttype=text/csv
xivelyapiendpoint=.csv
nrf2l01address=AddressGoesHere
nrf2l01channel=ChannelGoesHere
nrf2l01datarate=0
channel1=Sensor1
channel2=Sensor2
channel3=Sensor3
channel4=Sensor4
channel5=Sensor5
...
...

The first byte of each (upto 32 byte) nRF24L01 message is used to determine the Xively channel.

For testing I used a simple *duino program which uploads temperature and humidity readings every 5 seconds. It’s not terribly efficient or elegant and is just to illustrate how to package up the data.

#include <RF24_config>
#include <nRF24L01.h>
#include <SPI.h>
#include <RF24.h>
#include "Wire.h"
#include <TH02_dev.h>

//UNO R3 with embedded coolness board
//RF24 radio(3, 7);
//devDuino  with onboard
RF24 radio(8, 7);

char payload[32] = "";
const uint64_t pipe = 0x3165736142LL; // Base1 pay attention to byte ordering and address length

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

  radio.begin();
  radio.setPALevel(RF24_PA_MAX);
  radio.setChannel(10);
  radio.enableDynamicPayloads();
  radio.openWritingPipe(pipe);

  radio.printDetails();

  /* Power up,delay 150ms,until voltage is stable */
  delay(150);

  TH02.begin();

  delay(1000);
}

void loop()
{
  float temperature = TH02.ReadTemperature();
  float humidity = TH02.ReadHumidity();

  radio.powerUp();

  payload[0] = 'A';
  dtostrf(temperature, 5, 1, &payload[1]);
  Serial.println(payload);
  boolean result = radio.write(payload, strlen(payload));
  if (result)
    Serial.println("T Ok...");
  else
    Serial.println("T failed.");

  payload[0] = 'B';
  dtostrf(humidity, 5, 1, &payload[1]);
  Serial.println(payload);
  result = radio.write(payload, strlen(payload));
  if (result)
    Serial.println("H Ok...");
  else
    Serial.println("H failed.");

  radio.powerDown();

  delay(5000);
}

The gateway code creates a thread for each call to the Xively REST API. (In future the code may need to limit the number of concurrent requests)

private void OnReceive(byte[] data)
{
   activityLed.Write(!activityLed.Read());

   // Ensure that we have a valid payload
   if ( data.Length == 0 )
   {
      Debug.Print( "ERROR - Message has no payload" ) ;
      return ;
   }

   // Extract the device id
   string deviceId = xivelyApiChannleIDPrefix + data[0].ToString();
   string message = new String(Encoding.UTF8.GetChars(data, 1, data.Length - 1));

   string xivelyApiChannel = appSettings.GetString( deviceId, string.Empty ) ;
   if ( xivelyApiChannel.Length == 0 )
   {
      Debug.Print("ERROR - Inbound message has unknown channel " + deviceId);
      return ;
   }
   Debug.Print(DateTime.Now.ToString("HH:mm:ss") + " " + xivelyApiChannel + " " + message); ;

   Thread thread = new Thread(() =&gt; xivelyFeedUpdate(xivelyApiChannel, message ));
   thread.Start();
   }

private void xivelyFeedUpdate( string channel, string value)
{
   #region Assertions
   Debug.Assert(channel != null);
   Debug.Assert(channel != string.Empty );
   Debug.Assert(value != null);
   #endregion

   try
   {
      WebProxy webProxy = null;

      if (webProxyAddress.Length &gt; 1)
      {
         webProxy = new WebProxy(webProxyAddress, webProxyPort);
      }

      using (HttpWebRequest request = (HttpWebRequest)WebRequest.Create(xivelyApiBaseUrl + xivelyApiFeedID + xivelyApiEndpoint))
      {
         byte[] buffer = Encoding.UTF8.GetBytes(channel + "," + value);

         DateTime httpRequestedStartedAtUtc = DateTime.UtcNow;

         if (webProxy != null)
         {
            request.Proxy = webProxy;
         }
         request.Method = "PUT";
         request.ContentLength = buffer.Length;
         request.ContentType = xivelyApiContentType;
         request.Headers.Add("X-ApiKey", xivelyApiKey);
         request.KeepAlive = false;
         request.Timeout = httpRequestTimeoutmSec;
         request.ReadWriteTimeout = httpRequestReadWriteTimeoutmSec;

         // request body
         Debug.Print("HTTP request");
         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());
      }
   }
   catch (Exception ex)
   {
      Debug.Print(ex.Message);
   }
}

To use this code download the Nordic nRF24L01 library from Codeplex then include that plus my Netduino NRF24L01 Xively Gateway in a new solution and it should just work.

Deploy the application to a Netduino 2 Plus or Netduino 3 Wifi device and run it to create the app.config file, then use a text editor to update the file with your Xively & device settings.

I’ll upload this and a couple of other projects to GitHub shortly.

Bill of materials (prices as at July 2015)