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

Maduino LoRa Radio 868MHz Payload Addressing client

This is a demo MakerFabs Maduino LoRa Radio 868MHz client (based on one of the examples from Arduino-LoRa) that uploads telemetry data to my Windows 10 IoT Core on Raspberry PI AdaFruit.IO and Azure IoT Hub field gateways.

The code is available on Github

MaduinoLoRa86820180914
/*
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#includeconst int csPin = 10; // LoRa radio chip selectconst int resetPin = 9; // LoRa radio resetconst int irqPin = 2; // change for your board; must be a hardware interrupt pin// Field gateway configurationconst char FieldGatewayAddress[] = "LoRaIoT1";const float FieldGatewayFrequency = 915000000.0;//const float FieldGatewayFrequency = 433000000.0;const byte FieldGatewaySyncWord = 0x12 ;// Payload configurationconst int PayloadSizeMaximum = 64 ;byte payload[PayloadSizeMaximum] = "";const byte SensorReadingSeperator = ',' ;// Manual serial number configurationconst char DeviceId[] = {"Maduino1"};const int LoopSleepDelaySeconds = 10 ;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) << 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, (char*)&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, (char *)&[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 debugging output the data looked like this

13:40:28-RX From Maduino1 PacketSnr 9.8 Packet RSSI -65dBm RSSI -110dBm = 11 byte message "t 33.7,h 51"
 Sensor Maduino1t Value 33.7
 Sensor Maduino1h Value 51
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish
The thread 0x268 has exited with code 0 (0x0).
The thread 0xb28 has exited with code 0 (0x0).
13:40:38-RX From Maduino1 PacketSnr 9.5 Packet RSSI -66dBm RSSI -112dBm = 11 byte message "t 33.9,h 51"
 Sensor Maduino1t Value 33.9
 Sensor Maduino1h Value 51
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish
13:40:49-RX From Maduino1 PacketSnr 9.5 Packet RSSI -66dBm RSSI -110dBm = 11 byte message "t 34.0,h 51"
 Sensor Maduino1t Value 34.0
 Sensor Maduino1h Value 51
 AzureIoTHubClient SendEventAsync start
 AzureIoTHubClient SendEventAsync finish


Bill of materials (Prices Sep 2018)


  • Maduino LoRa Radion (868MHz) USD14.10
  • Seeedstudio Temperature&Humidity Sensor USD11.50
  • 4 pin Female Jumper to Grove 4 pin Conversion Cable USD2.90
  • 1 Watt solar panel with wires USD3.80
  • 3000 mAh LI-Ion battery

There is also a 433MHz version available at the same price


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 onboard sockets for battery and charging make the device easier to package and power in the field.


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


Then in my Azure IoT Hub monitoring software


MaduinoLoRaAzureIoT20180914
			

RFM9X.IoTCore Uputronics Raspberry PI LoRa(TM) Expansion Board

The Raspberry Pi+ LoRa(TM) Expansion Board has two RF modules. In my setup CE0 was 915MHz and CE1 was 433MHz so I modified the demo application so I could run both ports independently or simultaneously.

#if UPUTRONICS_RPIPLUS_CS0 && !UPUTRONICS_RPIPLUS_CS1
private const byte InterruptLine = 25;
private Rfm9XDevice rfm9XDevice = new Rfm9XDevice(ChipSelectPin.CS0, InterruptLine);
#endif
#if !UPUTRONICS_RPIPLUS_CS0 && UPUTRONICS_RPIPLUS_CS1
private const byte InterruptLine = 16;
private Rfm9XDevice rfm9XDevice = new Rfm9XDevice(ChipSelectPin.CS1, InterruptLine);
#endif
#if UPUTRONICS_RPIPLUS_CS0 && UPUTRONICS_RPIPLUS_CS1 // 433MHz and 915MHz in my setup
private const byte InterruptLineCS0 = 25;
private Rfm9XDevice rfm9XDeviceCS0 = new Rfm9XDevice(ChipSelectPin.CS0, InterruptLineCS0);
private const byte InterruptLineCS1 = 16;
private Rfm9XDevice rfm9XDeviceCS1 = new Rfm9XDevice(ChipSelectPin.CS1, InterruptLineCS1);
#endif

The in the run method

#if UPUTRONICS_RPIPLUS_CS0 && UPUTRONICS_RPIPLUS_CS1
public void Run(IBackgroundTaskInstance taskInstance)
{
rfm9XDeviceCS0.Initialise(915000000.0, paBoost: true, rxPayloadCrcOn: true);
rfm9XDeviceCS1.Initialise(433000000.0, paBoost: true, rxPayloadCrcOn: true);
#if DEBUG
rfm9XDeviceCS0.RegisterDump();
rfm9XDeviceCS1.RegisterDump();
#endif

rfm9XDeviceCS0.OnReceive += Rfm9XDevice_OnReceive;
rfm9XDeviceCS1.OnReceive += Rfm9XDevice_OnReceive;
#if ADDRESSED_MESSAGES_PAYLOAD
rfm9XDeviceCS0.Receive(UTF8Encoding.UTF8.GetBytes(Environment.MachineName));
rfm9XDeviceCS1.Receive(UTF8Encoding.UTF8.GetBytes(Environment.MachineName));
#else
rfm9XDeviceCS0.Receive();
rfm9XDeviceCS1.Receive();
#endif
rfm9XDeviceCS0.OnTransmit += Rfm9XDevice_OnTransmit;
rfm9XDeviceCS1.OnTransmit += Rfm9XDevice_OnTransmit;

Task.Delay(10000).Wait();

while (true)
{
string messageText = string.Format("Hello from {0} ! {1}", Environment.MachineName, MessageCount);
MessageCount -= 1;

byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
Debug.WriteLine("{0:HH:mm:ss}-TX {1} byte message {2}", DateTime.Now, messageBytes.Length, messageText);
#if ADDRESSED_MESSAGES_PAYLOAD
this.rfm9XDeviceCS0.Send(UTF8Encoding.UTF8.GetBytes("Netduino"), messageBytes);
this.rfm9XDeviceCS1.Send(UTF8Encoding.UTF8.GetBytes("Arduino1"), messageBytes);
#else
this.rfm9XDeviceCS0.Send(messageBytes);
this.rfm9XDeviceCS1.Send(messageBytes);
#endif
Task.Delay(10000).Wait();
}
}

#else

public void Run(IBackgroundTaskInstance taskInstance)
{
rfm9XDevice.Initialise(433000000, paBoost: true, rxPayloadCrcOn : true);
rfm9XDevice.Initialise(915000000, paBoost: true, rxPayloadCrcOn : true);
#if DEBUG
rfm9XDevice.RegisterDump();
#endif

rfm9XDevice.OnReceive += Rfm9XDevice_OnReceive;
#if ADDRESSED_MESSAGES_PAYLOAD
rfm9XDevice.Receive(UTF8Encoding.UTF8.GetBytes(Environment.MachineName));
#else
rfm9XDevice.Receive();
#endif
rfm9XDevice.OnTransmit += Rfm9XDevice_OnTransmit;

Task.Delay(10000).Wait();

while (true)
{
string messageText = string.Format("Hello from {0} ! {1}", Environment.MachineName, MessageCount);
MessageCount -= 1;

byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
Debug.WriteLine("{0:HH:mm:ss}-TX {1} byte message {2}", DateTime.Now, messageBytes.Length, messageText);
#if ADDRESSED_MESSAGES_PAYLOAD
this.rfm9XDevice.Send(UTF8Encoding.UTF8.GetBytes("AddressHere"), messageBytes);
#else
this.rfm9XDevice.Send(messageBytes);
#endif
Task.Delay(10000).Wait();
}
}
#endif

In the debugger output I could see addressed messages being sent to and arriving from a 433MHz Arduino device and a 915MHz Netduino device.

The thread 0x5f8 has exited with code 0 (0x0).
22:06:44-RX From Netduino PacketSnr 9.5 Packet RSSI -49dBm RSSI -110dBm = 20 byte message "Hello NetMF LoRa! 82"
22:06:47-RX From Arduino1 PacketSnr 9.8 Packet RSSI -56dBm RSSI -101dBm = 11 byte message "t 20.2,h 90"
22:06:48-TX 25 byte message Hello from LoRaIoT1 ! 163
22:06:49-TX Done
22:06:49-TX Done
The thread 0xe9c has exited with code 0 (0x0).
22:06:54-RX From Netduino PacketSnr 9.8 Packet RSSI -46dBm RSSI -111dBm = 20 byte message "Hello NetMF LoRa! 83"
22:06:57-RX From Arduino1 PacketSnr 9.8 Packet RSSI -61dBm RSSI -93dBm = 11 byte message "t 20.3,h 90"
22:06:58-TX 25 byte message Hello from LoRaIoT1 ! 162
22:06:59-TX Done
22:06:59-TX Done
The thread 0xf88 has exited with code 0 (0x0).
22:07:04-RX From Netduino PacketSnr 9.5 Packet RSSI -48dBm RSSI -110dBm = 20 byte message "Hello NetMF LoRa! 84"
22:07:07-RX From Arduino1 PacketSnr 9.8 Packet RSSI -61dBm RSSI -93dBm = 11 byte message "t 20.2,h 90"
22:07:09-TX 25 byte message Hello from LoRaIoT1 ! 161
22:07:09-TX Done
22:07:09-TX Done

This particular configuration has not been extensively tested yet and should be treated as early Beta (11 Sept 2018)

 

Uputronics Raspberry Pi+ LoRa(TM) Expansion Board

The second package to arrive was a Raspberry Pi+ LoRa(TM) Expansion Board populated with HopeRF 434MHz & 915MHz modules. It was in a small cardboard box with bolts+spacers and had a small set of printed instructions.

The shield has four user controlable Light Emitting Diodes(LED) connected to General Purpose Input Output(GPIO) pins which will be useful  for providing feedback when trying to debug faults etc..

uputronicsPiPlusHelp

Some of the pin numbers are also printed on the shield silk screen.UputronicsRPIPlusShield
This time the first step was to check the pin assignments of the 4 LEDs

//---------------------------------------------------------------------------------
// Copyright (c) September 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.
//
//---------------------------------------------------------------------------------
namespace devMobile.IoT.Rfm9x.UputronicsRPIPlusLed
{
using System;
using System.Threading;
using Windows.ApplicationModel.Background;
using Windows.Devices.Gpio;

public sealed class StartupTask : IBackgroundTask
{
public void Run(IBackgroundTaskInstance taskInstance)
{
GpioController gpioController = GpioController.GetDefault();

GpioPin ce01LedPin = gpioController.OpenPin(5);
ce01LedPin.SetDriveMode(GpioPinDriveMode.Output);
ce01LedPin.Write(GpioPinValue.Low);

GpioPin ceo2LedPin = gpioController.OpenPin(21);
ceo2LedPin.SetDriveMode(GpioPinDriveMode.Output);
ceo2LedPin.Write(GpioPinValue.High);

GpioPin lanLedPin = gpioController.OpenPin(6);
lanLedPin.SetDriveMode(GpioPinDriveMode.Output);
lanLedPin.Write(GpioPinValue.Low);

GpioPin internetLedPin = gpioController.OpenPin(13);
internetLedPin.SetDriveMode(GpioPinDriveMode.Output);
internetLedPin.Write(GpioPinValue.High);

while (true)
{
if (ce01LedPin.Read() == GpioPinValue.High)
{
ce01LedPin.Write(GpioPinValue.Low);
}
else
{
ce01LedPin.Write(GpioPinValue.High);
}

if (ceo2LedPin.Read() == GpioPinValue.High)
{
ceo2LedPin.Write(GpioPinValue.Low);
}
else
{
ceo2LedPin.Write(GpioPinValue.High);
}

if (lanLedPin.Read() == GpioPinValue.High)
{
lanLedPin.Write(GpioPinValue.Low);
}
else
{
lanLedPin.Write(GpioPinValue.High);
}

if (internetLedPin.Read() == GpioPinValue.High)
{
internetLedPin.Write(GpioPinValue.Low);
}
else
{
internetLedPin.Write(GpioPinValue.High);
}

Thread.Sleep(500);
}
}
}
}

I think there is a small issue with the internet LED it should be GPIO13 (which matches the pin number)

The next step was to get the Serial Peripheral Interface (SPI) interface for both modules working.

//---------------------------------------------------------------------------------
// Copyright (c) September 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.
//
//---------------------------------------------------------------------------------
namespace devMobile.IoT.Rfm9x.UputronicsRPIPlusSPI
{
using System;
using System.Diagnostics;
using System.Threading;
using Windows.ApplicationModel.Background;
using Windows.Devices.Spi;

public sealed class StartupTask : IBackgroundTask
{
public void Run(IBackgroundTaskInstance taskInstance)
{
#if CS0
const int chipSelectPinNumber = 0;
#endif
#if CS1
const int chipSelectPinNumber = 1;
#endif
SpiController spiController = SpiController.GetDefaultAsync().AsTask().GetAwaiter().GetResult();
var settings = new SpiConnectionSettings(chipSelectPinNumber)
{
ClockFrequency = 500000,
Mode = SpiMode.Mode0,   // From SemTech docs pg 80 CPOL=0, CPHA=0
};
SpiDevice Device = spiController.GetDevice(settings);

while (true)
{
byte[] writeBuffer = new byte[] { 0x42 }; // RegVersion
byte[] readBuffer = new byte[1];

// Read the RegVersion silicon ID to check SPI works
Device.TransferSequential(writeBuffer, readBuffer);

#if CS0
Debug.WriteLine("CS0 Register RegVer 0x{0:x2} - Value 0X{1:x2} - Bits {2}", writeBuffer[0], readBuffer[0], Convert.ToString(readBuffer[0], 2).PadLeft(8, '0'));
#endif
#if CS1
Debug.WriteLine("CS1 Register RegVer 0x{0:x2} - Value 0X{1:x2} - Bits {2}", writeBuffer[0], readBuffer[0], Convert.ToString(readBuffer[0], 2).PadLeft(8, '0'));
#endif
Thread.Sleep(10000);
}
}
}
}

Like the other uputronics shield I have tested this appears not to have the reset line of the RFM9X connected.

The output confirmed the code worked with both CS0 and CS1 defined

CS0 Register RegVer 0x42 - Value 0X12 - Bits 00010010
CS0 Register RegVer 0x42 - Value 0X12 - Bits 00010010
CS0 Register RegVer 0x42 - Value 0X12 - Bits 00010010
CS0 Register RegVer 0x42 - Value 0X12 - Bits 00010010
CS1 Register RegVer 0x42 - Value 0X12 - Bits 00010010
CS1 Register RegVer 0x42 - Value 0X12 - Bits 00010010
CS1 Register RegVer 0x42 - Value 0X12 - Bits 00010010

Would have been more useful to read RegFrMsb = 0x06, RegFrMid = 0x7, and RegFrLsb = 0x08 so I could see the different default frequencies of the two HopeRF modules. The next step is to build support for this shield into my RFM9X.IoTCore library.

RFM9X.NetMF on Github

After a month of posts the source code of V0.9 of my RFM9X/SX127X library is on GitHub. I included all of the source for my test harness and proof of concept(PoC) applications so other people can follow along with “my learning experience”.

I need to trial with some more hardware, frequency bands, variety of clients, initialisation configurations and backport the last round of fixes from my .Net library.

The simplest possible application .NetMF using the new library

/---------------------------------------------------------------------------------
// Copyright (c) August 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.
//
//---------------------------------------------------------------------------------
namespace devMobile.IoT.NetMF.Rfm9X.Client
{
using System;
using System.Text;
using System.Threading;
using devMobile.IoT.NetMF.ISM;
using Microsoft.SPOT;
using SecretLabs.NETMF.Hardware.Netduino;

public class Program
{
public static void Main()
{
Rfm9XDevice rfm9XDevice = new Rfm9XDevice(Pins.GPIO_PIN_D10, Pins.GPIO_PIN_D9, Pins.GPIO_PIN_D2);
byte MessageCount = Byte.MinValue;

rfm9XDevice.Initialise( Rfm9XDevice.RegOpModeMode.ReceiveContinuous, 915000000, paBoost: true, rxPayloadCrcOn: true);
rfm9XDevice.OnDataReceived += rfm9XDevice_OnDataReceived;
rfm9XDevice.OnTransmit += rfm9XDevice_OnTransmit;

while (true)
{
string messageText = "Hello NetMF LoRa! " + MessageCount.ToString();
MessageCount += 1;
byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
Debug.Print("Sending " + messageBytes.Length + " bytes message " + messageText);
rfm9XDevice.SendMessage(messageBytes);

Thread.Sleep(10000);
}
}

static void rfm9XDevice_OnTransmit()
{
Debug.Print("Transmit-Done");
}

static void rfm9XDevice_OnDataReceived(byte[] data)
{
try
{
string messageText = new string(UTF8Encoding.UTF8.GetChars(data));

Debug.Print("Received " + data.Length.ToString() + " byte message " + messageText);
}
catch (Exception ex)
{
Debug.Print(ex.Message);
}
}
}
}

// Dirty hack for Rosyln
namespace System.Diagnostics
{
public enum DebuggerBrowsableState
{
Never = 0,
Collapsed = 2,
RootHidden = 3
}
}

I need to do more testing (especially of the initialisation options) and will add basic device addressing soon so my field gateway will only see messages which it is interested in.

RFM9X.IoTCore on Github

After a month of posts the source code of V0.9 of my RFM9X/SX127X library is on GitHub. I included all of the source for my test harness and proof of concept(PoC) applications so other people can follow along with “my learning experience”.

I started wanting a library to for a LoRa telemetry field gateway and ended up writing one (which is usually not a good idea). My use case was a device that was configured, then run for long periods of time, was not battery powered, and if settings were changed could be restarted. I need to trial with some more hardware, frequency bands, variety of clients, initialisation configurations and backport the last round of fixes to my .NetMF library.

I am also looking at writing an RFM69 library using a pair of shields (434MHz & 915MHz)  from seegel-systeme.

The simplest possible application using the new library (a fair bit of the code is to support the different supported shields)

//---------------------------------------------------------------------------------
// Copyright (c) August 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.
//
//---------------------------------------------------------------------------------
namespace devMobile.IoT.Rfm9x.LoRaDeviceClient
{
using System;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;

using devMobile.IoT.Rfm9x;
using Windows.ApplicationModel.Background;

public sealed class StartupTask : IBackgroundTask
{
private byte NessageCount = Byte.MaxValue;
#if DRAGINO
private const byte ChipSelectLine = 25;
private const byte ResetLine = 17;
private const byte InterruptLine = 4;
private Rfm9XDevice rfm9XDevice = new Rfm9XDevice(ChipSelectPin.CS0, ChipSelectLine, ResetLine, InterruptLine);
#endif
#if M2M
private const byte ChipSelectLine = 25;
private const byte ResetLine = 17;
private const byte InterruptLine = 4;
private Rfm9XDevice rfm9XDevice = new Rfm9XDevice(ChipSelectPin.CS0, ChipSelectLine, ResetLine, InterruptLine);
#endif
#if ELECROW
private const byte ResetLine = 22;
private const byte InterruptLine = 25;
private Rfm9XDevice rfm9XDevice = new Rfm9XDevice(ChipSelectPin.CS1, ResetLine, InterruptLine);
#endif
#if ELECTRONIC_TRICKS
private const byte ResetLine = 22;
private const byte InterruptLine = 25;
private Rfm9XDevice rfm9XDevice = new Rfm9XDevice(ChipSelectPin.CS0, 22, 25);
#endif

public void Run(IBackgroundTaskInstance taskInstance)
{
rfm9XDevice.Initialise(Rfm9XDevice.RegOpModeMode.ReceiveContinuous, 915000000.0, paBoost: true);

#if DEBUG
rfm9XDevice.RegisterDump();
#endif
rfm9XDevice.OnReceive += Rfm9XDevice_OnReceive;
rfm9XDevice.OnTransmit += Rfm9XDevice_OnTransmit;

Task.Delay(10000).Wait();

while (true)
{
string messageText = string.Format("Hello W10 IoT Core LoRa! {0}", NessageCount);
NessageCount -= 1;

byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
Debug.WriteLine("{0:HH:mm:ss}-TX {1} byte message {2}", DateTime.Now, messageBytes.Length, messageText);
this.rfm9XDevice.Send(messageBytes);

Task.Delay(10000).Wait();
}
}

private void Rfm9XDevice_OnReceive(object sender, Rfm9XDevice.OnDataReceivedEventArgs e)
{
try
{
string messageText = UTF8Encoding.UTF8.GetString(e.Data);

Debug.WriteLine("{0:HH:mm:ss}-RX {1} byte message {2}", DateTime.Now, e.Data.Length, messageText);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}

private void Rfm9XDevice_OnTransmit(object sender, Rfm9XDevice.OnDataTransmitedEventArgs e)
{
Debug.WriteLine("{0:HH:mm:ss}-TX Done", DateTime.Now);
}
}
}

I have a shield from uputronics on order which should arrive from the UK in roughly a week. This shield has two RFM9X devices onboard (In my case 434MHz & 915MHz) so it will be interesting to see how my library copes with two instances of the stack running together.

I need to do more testing (especially of the initialisation options) and will add basic device addressing soon so my field gateway will only see messages which it is interested in.

Windows 10 IoT Core LoRa library

I have a pair of Windows 10 IoT Core nRF24L01 field gateway projects, one for AdaFruit.IO and the other for Azure IoTHub (Including Azure IoT Central). I use these field gateways for small scale indoor and outdoor deployments.

For larger systems e.g a school campus I was looking for something with a bit more range (line of site + in building penetration) and clients with lower power consumption (suitable for long term battery or solar power).

Other makers had had success with RFM69(proprietary) and RFM9X (LoRA) based devices and shields/hats so I had a look at both technologies.

To kick things off I purchased

I then did some searching and downloaded two commonly used libraries

Initially I trialled the emmellsoft Windows 10 IoT Core Dragino.LoRa code on a couple of Raspberry PI devices.

RPIDraginoP2P

After updating the Windows 10 Min/Max versions, plus the NuGet packages and setting the processor type to ARM the code compiled, downloaded and ran which was a pretty good start.

I could see messages being transmitted and received by the two devices


Packet RSSI: -33, RSSI: -91, SNR: 8, Length: 5

Message Received: CRC OK, Rssi=-91, PacketRssi=-33, PacketSnr=8, Buffer:[55, ff, 00, aa, 01], 2018-07-30 09:27:48

Successfully sent in 110 milliseconds.

Packet RSSI: -15, RSSI: -100, SNR: 9.2, Length: 5

Message Received: CRC OK, Rssi=-100, PacketRssi=-15, PacketSnr=9.2, Buffer:[55, ff, 00, aa, 02], 2018-07-30 09:27:53

Successfully sent in 36 milliseconds.

Packet RSSI: -35, RSSI: -101, SNR: 9, Length: 5

Message Received: CRC OK, Rssi=-101, PacketRssi=-35, PacketSnr=9, Buffer:[55, ff, 00, aa, 03], 2018-07-30 09:27:58

Successfully sent in 36 milliseconds.

I added my first attempt at device configuration for New Zealand (based on EU settings) in Dragino.LoRa\Radio\TransceiverSettings.cs


public static readonly TransceiverSettings ANZ915 = new TransceiverSettings(

RadioModemKind.Lora,

915000000,

BandWidth.BandWidth_125_00_kHz,

SpreadingFactor.SF7,

CodingRate.FourOfFive,

8,

true,

false,

LoraSyncWord.Public);

The LoraSyncWord.Public would turn out to be a problem later!

Then I modified the sender and receiver sample application MainPage.xaml.cs files to reference my settings


private static TransceiverSettings GetRadioSettings()

{

// *********************************************************************************************

// #1/2. YOUR EDITING IS REQUIRED HERE!

//

// Choose transeiver settings:

// *********************************************************************************************

return TransceiverSettings.Standard.ANZ915;

}

I modified one of the RadioHead sample Arduino applications (centre frequency) and deployed it to a LoRa MiniDev device. I could see messages getting sent but they were not getting received by the RPI(s).

So I dumped the registers for the SX127X device in the HopeRF RFM95 module on both devices,

DraginoLoraMinDev

From the device on RPI Hat


SX1276/77/78/79 detected, starting.

1-85

2-1A

3-B

4-0

5-52

6-E4

7-C0

8-0

9-85

A-9

B-2B

C-23

D-0

E-80

F-0

10-0

11-0

12-0

13-0

14-0

15-0

16-0

17-0

18-4

19-0

1A-0

1B-42

1C-0

1D-72

1E-74

1F-9F

20-0

21-8

22-1

23-FF

24-0

25-0

26-4

27-0

28-0

29-0

2A-0

2B-0

2C-9

2D-50

2E-14

2F-45

30-55

31-C3

32-5

33-27

34-1C

35-A

36-3

37-A

38-42

39-34

The LoRa transceiver is initiated successfully.

I printed out the Radiohead and emmellsoft registers then manually compared them using the SX1276 datasheet for reference.

I found the 3 registers which contain the MSB, ISB and LSB for the centre frequency weren’t being calculated correctly (checked this by changing the frequency to 434MHz and comparing the register values to the worked example in the datasheet).

I manually “brute forced” the centre frequency registers in LoRaTransceiver.cs Init() and the RPI could then detect a signal but couldn’t decode the messages.

I went back to the Register dumps and found the SyncWord (odd name as it is a byte) were different. After updating the RPI settings the devices could exchange packets..


const double RH_RF95_FXOSC = 32000000.0;

const double RH_RF95_FSTEP = RH_RF95_FXOSC / 524288.0;

long frf = (long)(Settings.Frequency / RH_RF95_FSTEP);

byte[] bytes = BitConverter.GetBytes(frf);

byte[] x6 = { bytes[2] };

RegisterManager.WriteRegister(6, x6);

byte[] x7 = { bytes[1] };

RegisterManager.WriteRegister(7, x7);

byte[] x8 = { bytes[0] };

RegisterManager.WriteRegister(8, x8);

RegisterManager.Write(new LoraRegisterSyncWord(Settings.LoraSyncWord.Value));

This was not a long term solution, lots of code, and register setting changes with limited explanation…