IoT.Net LoRa Radio 915 MHz Payload Addressing client

This is a demo ingenuity micro IoT.Net client (based on one of the examples in my RFM9XLoRaNetMF library) that uploads telemetry data to my Windows 10 IoT Core on Raspberry PI field gateway. 

Thought the silk screen says RFM69 this is a prototype running an RFM95 module.

iotnetlora.jpg

Bill of materials (Prices Sep 2018)

  • IoT.Net device (Beta tester will add price when available)

The device has an onboard MCP9808 temperature sensor which kept the BoM really short. I have had to make some modifications to my RFM9XLoRaNetMF library as the IoT.Net device uses a different SPI port. The code for this devices and the changes will be uploaded to GitHub in the next couple of days.

//---------------------------------------------------------------------------------
// Copyright (c) Sept 2018, devMobile Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// git remote add origin https://github.com/KiwiBryn/FieldGateway.LoRa.IoTNetClient.git
// git push -u origin master
//---------------------------------------------------------------------------------
namespace devMobile.IoT.IoTNet.FieldGateway
{
	using System;
	using System.Text;
	using System.Threading;
	using Microsoft.SPOT;
	using Microsoft.SPOT.Hardware;
	using devMobile.IoT.NetMF.ISM;
	using IngenuityMicro.Sensors;

	class IoTNetClient
	{
		private readonly Rfm9XDevice rfm9XDevice;
		private readonly TimeSpan dueTime = new TimeSpan(0, 0, 10);
		private readonly TimeSpan periodTime = new TimeSpan(0, 0, 30);
		private readonly MCP9808 mcp9808 = new MCP9808();
		private readonly OutputPort _led = new OutputPort((Cpu.Pin)16 + 8, false);
		private readonly byte[] fieldGatewayAddress = Encoding.UTF8.GetBytes("LoRaIoT1");
		private readonly byte[] deviceAddress = Encoding.UTF8.GetBytes("IoTNet1");

		public IoTNetClient()
		{
			rfm9XDevice = new Rfm9XDevice( SPI.SPI_module.SPI3, (Cpu.Pin)16 + 9, (Cpu.Pin)5, (Cpu.Pin)4);
		}

		public void Run()
		{
			rfm9XDevice.Initialise(frequency: 915000000, paBoost: true, rxPayloadCrcOn: true);
			rfm9XDevice.Receive(deviceAddress);

			rfm9XDevice.OnDataReceived += rfm9XDevice_OnDataReceived;
			rfm9XDevice.OnTransmit += rfm9XDevice_OnTransmit;

			Timer temperatureUpdates = new Timer(TemperatureTimerProc, null, dueTime, periodTime);

			Thread.Sleep(Timeout.Infinite);
		}

		private void TemperatureTimerProc(object state)
		{
			_led.Write(true);

			double temperature = mcp9808.ReadTempInC();

			Debug.Print(DateTime.UtcNow.ToString("hh:mm:ss") + "  T:" + temperature.ToString("F1"));

			rfm9XDevice.Send(fieldGatewayAddress, Encoding.UTF8.GetBytes("t " + temperature.ToString("F1")));

			_led.Write(true);
		}

		void rfm9XDevice_OnTransmit()
		{
			Debug.Print("Transmit-Done");
			_led.Write(false);
		}

		void rfm9XDevice_OnDataReceived(byte[] address, float packetSnr, int packetRssi, int rssi, byte[] data)
		{
			try
			{
				string messageText = new string(UTF8Encoding.UTF8.GetChars(data));
				string addressText = new string(UTF8Encoding.UTF8.GetChars(address));

				Debug.Print(DateTime.UtcNow.ToString("HH:MM:ss") + "-Rfm9X PacketSnr " + packetSnr.ToString("F1") + " Packet RSSI " + packetRssi + "dBm RSSI " + rssi + "dBm = " + data.Length + " byte message " + @"""" + messageText + @"""");
			}
			catch (Exception ex)
			{
				Debug.Print(ex.Message);
			}
		}
	}
}
}

.Net Framework debug output Field Gateway

22:55:39-RX From IoTNet1 PacketSnr 9.5 Packet RSSI -50dBm RSSI -110dBm = 6 byte message "t 23.6"
 Sensor IoTNet1t Value 23.6
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish
The thread 0xbec has exited with code 0 (0x0).
The thread 0xbb4 has exited with code 0 (0x0).
The thread 0xa0c has exited with code 0 (0x0).
The thread 0x13c has exited with code 0 (0x0).
22:56:09-RX From IoTNet1 PacketSnr 9.3 Packet RSSI -44dBm RSSI -102dBm = 6 byte message "t 23.8"
 Sensor IoTNet1t Value 23.8
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish

A small footprint, battery powered .NetMF 4.4 LoRa device designed and made in New Zealand with Visual Studio 2017 support is great.

LoRa Radio Node v1.0 868/915MHz Payload Addressing Client

This is a demo IoTMCU LoRa Radio Node V1.0 868/915MHz client (based on one of the examples from Arduino-LoRa) that uploads telemetry data to my Windows 10 IoT Core on Raspberry PI field gateway proof of concept(PoC).

The devices arrived promptly and the sample code and schematics made adapting my Arduino code easy.

Bill of materials (Prices Sep 2018)

The code is pretty basic, it shows how to pack the payload and set the necessary RFM9X/SX127X LoRa module configuration, has no power conservation, advanced wireless configuration etc.

IoTMCULoRa915V2

/*
  Adapted from LoRa Duplex communication with Sync Word

  Sends temperature & humidity data from Seeedstudio 

  https://www.seeedstudio.com/Grove-Temperature-Humidity-Sensor-High-Accuracy-Min-p-1921.html

  To my Windows 10 IoT Core RFM 9X library

  https://blog.devmobile.co.nz/2018/09/03/rfm9x-iotcore-payload-addressing/
*/
#include               // include libraries
#include
#include
const int csPin = 10;          // LoRa radio chip select
const int resetPin = 9;       // LoRa radio reset
const int irqPin = 2;         // change for your board; must be a hardware interrupt pin

// Field gateway configuration
const char FieldGatewayAddress[] = "LoRaIoT1";
const float FieldGatewayFrequency =  915000000.0;
const byte FieldGatewaySyncWord = 0x12 ;

// Payload configuration
const int PayloadSizeMaximum = 64 ;
byte payload[PayloadSizeMaximum] = "";
const byte SensorReadingSeperator = ',' ;

// Manual serial number configuration
const char DeviceId[] = {"IoTMCU915"};

const int LoopSleepDelaySeconds = 60 ;

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

  Serial.println("LoRa Setup");

  // override the default CS, reset, and IRQ pins (optional)
  LoRa.setPins(csPin, resetPin, irqPin);// set CS, reset, IRQ pin

  if (!LoRa.begin(FieldGatewayFrequency))
  {
    Serial.println("LoRa init failed. Check your connections.");
    while (true);
  }

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

  //LoRa.dumpRegisters(Serial);
  Serial.println("LoRa Setup done.");

  // Configure the Seeedstudio TH02 temperature & humidity sensor
  Serial.println("TH02 setup");
  TH02.begin();
  delay(100);
  Serial.println("TH02 Setup done");  

  Serial.println("Setup done");
}

void loop()
{
  int payloadLength = 0 ;
  float temperature ;
  float humidity ;

  Serial.println("Loop called");
  memset(payload, 0, sizeof(payload));

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

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

  // Copy the "From" into payload
  memcpy(&payload[payloadLength], DeviceId, strlen(DeviceId));
  payloadLength += strlen(DeviceId) ;

  // Read the temperature and humidity values then display nicely
  temperature = TH02.ReadTemperature();
  humidity = TH02.ReadHumidity();

  Serial.print("T:");
  Serial.print( temperature, 1 ) ;
  Serial.print( "C" ) ;

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

  // Copy the temperature into the payload
  payload[ payloadLength] = 't';
  payloadLength += 1 ;
  payload[ payloadLength] = ' ';
  payloadLength += 1 ;
  payloadLength += strlen( dtostrf(temperature, -1, 1, &payload[payloadLength]));
  payload[ payloadLength] = SensorReadingSeperator;
  payloadLength += sizeof(SensorReadingSeperator) ;

  // Copy the humidity into the payload
  payload[ payloadLength] = 'h';
  payloadLength += 1 ;
  payload[ payloadLength] = ' ';
  payloadLength += 1 ;
  payloadLength += strlen( dtostrf(humidity, -1, 0, &payload[payloadLength]));  

  // display info about payload then send it (No ACK) with LoRa unlike nRF24L01
  Serial.print( "RFM9X/SX127X Payload length:");
  Serial.print( payloadLength );
  Serial.println( " bytes" );

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

  Serial.println("Loop done");

  delay(LoopSleepDelaySeconds * 1000l);
}

In the debug output window the messages from the device looked like this

Register 0x40 – Value 0X00 – Bits 00000000
Register 0x41 – Value 0X00 – Bits 00000000
Register 0x42 – Value 0X12 – Bits 00010010

The thread 0x6f8 has exited with code 0 (0x0).
The thread 0x2f0 has exited with code 0 (0x0).
19:35:35-RX From IoTMCU915 PacketSnr 10.0 Packet RSSI -45dBm RSSI -109dBm = 11 byte message "t 19.8,h 88"
Sensor IoTMCU915t Value 19.8
Sensor IoTMCU915h Value 88
AzureIoTHubClient SendEventAsync start
AzureIoTHubClient SendEventAsync finish
The thread 0x9b0 has exited with code 0 (0x0).
The thread 0x9f4 has exited with code 0 (0x0).
The thread 0x9dc has exited with code 0 (0x0).
The thread 0x17fc has exited with code 0 (0x0).
The thread 0x944 has exited with code 0 (0x0).
19:36:35-RX From IoTMCU915 PacketSnr 10.8 Packet RSSI -45dBm RSSI -108dBm = 11 byte message "t 19.7,h 88"
Sensor IoTMCU915t Value 19.7
Sensor IoTMCU915h Value 88
AzureIoTHubClient SendEventAsync start
AzureIoTHubClient SendEventAsync finish
The thread 0xbf0 has exited with code 0 (0x0).

I have some suitable batteries on order from Jaycar a local supplier. There is also a 433MHz version of this device available other regions.

Netduino LoRa Radio 433/868/915 MHz Payload Addressing client

This is a demo Netduino client (based on one of the examples in my RFM9XLoRaNetMF library) that uploads telemetry data to my Windows 10 IoT Core on Raspberry PI field gateway proof of concept(PoC).

Bill of materials (Prices Sep 2018)

//---------------------------------------------------------------------------------
// Copyright (c) 2017, devMobile Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//---------------------------------------------------------------------------------
namespace devMobile.IoT.Netduino.FieldGateway
{
   using System;
   using System.Text;
   using System.Threading;
   using Microsoft.SPOT;
   using Microsoft.SPOT.Hardware;
   using SecretLabs.NETMF.Hardware.Netduino;
   using devMobile.IoT.NetMF.ISM;
   using devMobile.NetMF.Sensor;

   class NetduinoClient
   {
      Rfm9XDevice rfm9XDevice;
      private readonly TimeSpan dueTime = new TimeSpan(0, 0, 15);
      private readonly TimeSpan periodTime = new TimeSpan(0, 0, 300);
      private readonly SiliconLabsSI7005 sensor = new SiliconLabsSI7005();
      private readonly OutputPort _led = new OutputPort(Pins.ONBOARD_LED, false);
      private readonly byte[] fieldGatewayAddress = Encoding.UTF8.GetBytes("LoRaIoT1");
      private readonly byte[] deviceAddress = Encoding.UTF8.GetBytes("Netduino1");

      public NetduinoClient()
      {
         rfm9XDevice = new Rfm9XDevice(Pins.GPIO_PIN_D10, Pins.GPIO_PIN_D9, Pins.GPIO_PIN_D2);
      }

      public void Run()
      {
         //rfm9XDevice.Initialise(frequency: 915000000, paBoost: true, rxPayloadCrcOn: true);
         rfm9XDevice.Initialise(frequency: 433000000, paBoost: true, rxPayloadCrcOn: true);
         rfm9XDevice.Receive(deviceAddress);

         rfm9XDevice.OnDataReceived += rfm9XDevice_OnDataReceived;
         rfm9XDevice.OnTransmit += rfm9XDevice_OnTransmit;

         Timer humidityAndtemperatureUpdates = new Timer(HumidityAndTemperatureTimerProc, null, dueTime, periodTime);

         Thread.Sleep(Timeout.Infinite);
      }

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

         double humidity = sensor.Humidity();
         double temperature = sensor.Temperature();

         Debug.Print(DateTime.UtcNow.ToString("hh:mm:ss") + " H:" + humidity.ToString("F1") + " T:" + temperature.ToString("F1"));

         rfm9XDevice.Send(fieldGatewayAddress, Encoding.UTF8.GetBytes( "t " + temperature.ToString("F1") + ",H " + humidity.ToString("F0")));

         _led.Write(true);
      }

      void rfm9XDevice_OnTransmit()
      {
         Debug.Print("Transmit-Done");
         _led.Write(false);
      }

      void rfm9XDevice_OnDataReceived(byte[] address, float packetSnr, int packetRssi, int rssi, byte[] data)
      {
         try
         {
            string messageText = new string(UTF8Encoding.UTF8.GetChars(data));
            string addressText = new string(UTF8Encoding.UTF8.GetChars(address));

            Debug.Print(DateTime.UtcNow.ToString("HH:MM:ss") + "-Rfm9X PacketSnr " + packetSnr.ToString("F1") + " Packet RSSI " + packetRssi + "dBm RSSI " + rssi + "dBm = " + data.Length + " byte message " + @"""" + messageText + @"""");
         }
         catch (Exception ex)
         {
            Debug.Print(ex.Message);
         }
      }
   }
}

The code is available on GitHub
FieldGatewayNetduinoLoRaElecrow915
Elecrow shield
FieldGatewayNetduinoLoRaDragino915
Dragino shield
FieldGatewayNetduinLoRaMakerFabs433
MakerFabs shield
Net Micro Framework debug output from device

The thread '' (0x2) has exited with code 0 (0x0).
12:00:18 H:96.9 T:19.6
Transmit-Done
12:05:17 H:95.1 T:20.1
Transmit-Done

.Net Framework debug output Field Gateway

The thread 0x1550 has exited with code 0 (0x0).
21:21:49-RX From Netduino1 PacketSnr 9.5 Packet RSSI -40dBm RSSI -107dBm = 11 byte message "t 19.6,H 97"
 Sensor Netduino1t Value 19.6
 Sensor Netduino1H Value 97
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish
...
21:26:49-RX From Netduino1 PacketSnr 9.5 Packet RSSI -33dBm RSSI -103dBm = 11 byte message "t 20.1,H 95"
 Sensor Netduino1t Value 20.1
 Sensor Netduino1H Value 95
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish
The thread 0xfbc has exited with code 0 (0x0).

Then in my Azure IoT Hub

AzureIOTHubExplorerScreenGrab20180917

Dragino LoRaMiniDev Payload Addressing Client

This is a demo Dragino LoRa Mini Dev featuring LoRa® technology client (based on one of the examples from Arduino-LoRa) that uploads telemetry data to my AdaFruit.IO and Azure IoT Hubs Windows 10 IoT Core on Raspberry PI proof of concept (PoC) field gateways.

LoRaMiniDevTH02

Bill of materials (Prices Sep 2018)

  • Draguino LoRa MiniDev USD23
  • Seeedstudio Temperature&Humidity Sensor USD11.50 NZD20
  • 4 pin Male Jumper to Grove 4 pin Conversion Cable USD2.90

The code is pretty basic, it shows how to pack the payload and set the necessary RFM9X/SX127X LoRa module configuration, has no power conservation, advanced wireless configuration etc.

The Grove 4 pin Male Jumper to Grove 4 pin Conversion Cable was a quick & convenient way to get the I2C Grove temperature and humidity sensor connected up.

/*
  Adapted from LoRa Duplex communication with Sync Word

  Sends temperature & humidity data from Seeedstudio 

  https://www.seeedstudio.com/Grove-Temperature-Humidity-Sensor-High-Accuracy-Min-p-1921.html

  To my Windows 10 IoT Core RFM 9X library

  https://blog.devmobile.co.nz/2018/09/03/rfm9x-iotcore-payload-addressing/

*/
#include
#include
#include
const int csPin = 10;          // LoRa radio chip select
const int resetPin = 9;       // LoRa radio reset
const int irqPin = 2;         // change for your board; must be a hardware interrupt pin

// Field gateway configuration
const byte FieldGatewayAddress[] = "LoRaIoT1";
const float FieldGatewayFrequency =  915000000.0;
//const float FieldGatewayFrequency =  433000000.0;
const byte FieldGatewaySyncWord = 0x12 ;

// Payload configuration
const int PayloadSizeMaximum = 64 ;
char payload[PayloadSizeMaximum] = "";
const byte SensorReadingSeperator = ',' ;

// Manual serial number configuration
char DeviceId[] = {"LoRaMiniDev5"};

const int LoopSleepDelaySeconds = 10 ;

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

  Serial.print("LoRa Setup-");
  Serial.println( DeviceId ) ;

  // override the default CS, reset, and IRQ pins (optional)
  LoRa.setPins(csPin, resetPin, irqPin);// set CS, reset, IRQ pin

  if (!LoRa.begin(FieldGatewayFrequency))
  {
    Serial.println("LoRa init failed. Check your connections.");
    while (true);
  }

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

  //LoRa.dumpRegisters(Serial);
  Serial.println("LoRa Setup done.");

  // Configure the Seeedstudio TH02 temperature & humidity sensor
  Serial.println("TH02 setup");
  TH02.begin();
  delay(100);
  Serial.println("TH02 Setup done");  

  Serial.println("Setup done");
}

void loop()
{
  int payloadLength = 0 ;
  float temperature ;
  float humidity ;

  Serial.println("Loop called");
  memset(payload, 0, sizeof(payload));

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

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

  // Copy the "From" into payload
  memcpy(&payload[payloadLength], DeviceId, strlen(DeviceId));
  payloadLength += strlen(DeviceId) ;

  // Read the temperature and humidity values then display nicely
  temperature = TH02.ReadTemperature();
  humidity = TH02.ReadHumidity();

  Serial.print("T:");
  Serial.print( temperature, 1 ) ;
  Serial.print( "C" ) ;

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

  // Copy the temperature into the payload
  payload[ payloadLength] = 't';
  payloadLength += 1 ;
  payload[ payloadLength] = ' ';
  payloadLength += 1 ;
  payloadLength += strlen( dtostrf(temperature, -1, 1, &payload[payloadLength]));
  payload[ payloadLength] = SensorReadingSeperator;
  payloadLength += sizeof(SensorReadingSeperator) ;

  // Copy the humidity into the payload
  payload[ payloadLength] = 'h';
  payloadLength += 1 ;
  payload[ payloadLength] = ' ';
  payloadLength += 1 ;
  payloadLength += strlen( dtostrf(humidity, -1, 0, &payload[payloadLength]));  

  // display info about payload then send it (No ACK) with LoRa unlike nRF24L01
  Serial.print( "RFM9X/SX127X Payload length:");
  Serial.print( payloadLength );
  Serial.println( " bytes" );

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

  Serial.println("Loop done");

  delay(LoopSleepDelaySeconds * 1000l);
}

In the debug output window the messages from the device looked like this

09:53:05-RX From LoRaMiniDev5 PacketSnr 9.3 Packet RSSI -65dBm RSSI -109dBm = 11 byte message "t 16.8,h 98"
 Sensor LoRaMiniDev5t Value 16.8
 Sensor LoRaMiniDev5h Value 98
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish
The thread 0xba0 has exited with code 0 (0x0).
The thread 0xb24 has exited with code 0 (0x0).
09:53:15-RX From LoRaMiniDev5 PacketSnr 9.3 Packet RSSI -65dBm RSSI -108dBm = 11 byte message "t 16.7,h 98"
 Sensor LoRaMiniDev5t Value 16.7
 Sensor LoRaMiniDev5h Value 98
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish
The thread 0x76c has exited with code 0 (0x0).
The thread 0x91c has exited with code 0 (0x0).

Then in my Azure IoT Hub monitoring software
DraginoLoraMinDevEventHub
The dragino LoRa Mini Dev with an external antenna connector would be a good indoor data acquisition node for student project when powered by a 2nd hand cellphone charger.

Arduino LoRa Payload Addressing Client

This is a demo Arduino client (based on one of the examples from Arduino-LoRa) that uploads telemetry data to my Windows 10 IoT Core on Raspberry PI field gateway proof of concept(PoC).

Bill of materials (Prices Sep 2018)

ArduinoUnoR3DraginoLoRa

The code is pretty basic, it shows how to pack the payload and set the necessary RFM9X/SX127X LoRa module configuration, has no power conservation, advanced wireless configuration etc.

/*
  Adapted from LoRa Duplex communication with Sync Word

  Sends temperature & humidity data from Seeedstudio 

  https://www.seeedstudio.com/Grove-Temperature-Humidity-Sensor-High-Accuracy-Min-p-1921.html

  To my Windows 10 IoT Core RFM 9X library

  https://blog.devmobile.co.nz/2018/09/03/rfm9x-iotcore-payload-addressing/

*/
#include
#include
#include
const int csPin = 10;          // LoRa radio chip select
const int resetPin = 9;       // LoRa radio reset
const int irqPin = 2;         // change for your board; must be a hardware interrupt pin

// Field gateway configuration
const byte FieldGatewayAddress[] = "LoRaIoT1";
const float FieldGatewayFrequency =  915000000.0;
const byte FieldGatewaySyncWord = 0x12 ;

// Payload configuration
const int PayloadSizeMaximum = 64 ;
char payload[PayloadSizeMaximum] = "";
const byte SensorReadingSeperator = ',' ;

// Manual serial number configuration
char DeviceId[] = {"Arduino1"};

const int LoopSleepDelaySeconds = 60;

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

  Serial.println("LoRa Setup");

  // override the default CS, reset, and IRQ pins (optional)
  LoRa.setPins(csPin, resetPin, irqPin);// set CS, reset, IRQ pin

  if (!LoRa.begin(FieldGatewayFrequency))
  {
    Serial.println("LoRa init failed. Check your connections.");
    while (true);
  }

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

  //LoRa.dumpRegisters(Serial);
  Serial.println("LoRa Setup done.");

  // Configure the Seeedstudio TH02 temperature & humidity sensor
  Serial.println("TH02 setup");
  TH02.begin();
  delay(100);
  Serial.println("TH02 Setup done");  

  Serial.println("Setup done");
}

void loop()
{
  int payloadLength = 0 ;
  float temperature ;
  float humidity ;

  Serial.println("Loop called");
  memset(payload, 0, sizeof(payload));

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

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

  // Copy the "From" into payload
  memcpy(&payload[payloadLength], DeviceId, strlen(DeviceId));
  payloadLength += strlen(DeviceId) ;

  // Read the temperature and humidity values then display nicely
  temperature = TH02.ReadTemperature();
  humidity = TH02.ReadHumidity();

  Serial.print("T:");
  Serial.print( temperature, 1 ) ;
  Serial.print( "C" ) ;

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

  // Copy the temperature into the payload
  payload[ payloadLength] = 't';
  payloadLength += 1 ;
  payload[ payloadLength] = ' ';
  payloadLength += 1 ;
  payloadLength += strlen( dtostrf(temperature, -1, 1, &payload[payloadLength]));
  payload[ payloadLength] = SensorReadingSeperator;
  payloadLength += sizeof(SensorReadingSeperator) ;

  // Copy the humidity into the payload
  payload[ payloadLength] = 'h';
  payloadLength += 1 ;
  payload[ payloadLength] = ' ';
  payloadLength += 1 ;
  payloadLength += strlen( dtostrf(humidity, -1, 0, &payload[payloadLength]));  

  // display info about payload then send it (No ACK) with LoRa unlike nRF24L01
  Serial.print( "RFM9X/SX127X Payload length:");
  Serial.print( payloadLength );
  Serial.println( " bytes" );

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

  Serial.println("Loop done");

  delay(LoopSleepDelaySeconds * 1000l);
}

In the debug output window the messages from the device looked like this

Register 0x40 – Value 0X00 – Bits 00000000
Register 0x41 – Value 0X00 – Bits 00000000
Register 0x42 – Value 0X12 – Bits 00010010

19:15:21-RX From Arduino1 PacketSnr 9.3 Packet RSSI -49dBm RSSI -105dBm = 11 byte message "t 18.8,h 91"

19:15:30-TX 25 byte message Hello from LoRaIoT1 ! 255
19:15:30-TX Done
19:15:31-RX From Arduino1 PacketSnr 9.3 Packet RSSI -49dBm RSSI -103dBm = 11 byte message "t 18.8,h 91"

19:15:41-RX From Arduino1 PacketSnr 9.3 Packet RSSI -48dBm RSSI -106dBm = 11 byte message "t 18.8,h 91"

There must be a nicer way of building the payload, a topic for a future post maybe.

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

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