.NET nanoFramework RAK4200 LoRaWAN library OTAA Join

When I first tried Over The Air Activation(OTAA) to connect to The Things Network(TTN) with my RAKwireless RAK4200 module it didn’t work. I built another test harness with an FTDI module so I could send AT commands with the RAK Serial Port Tool to my RAK4200 module.

RAK4200 -> FTDI -? PC test harness

The default baud rate is 115200 but I sent “at+set_config=device:uart:1:9600” to the RAK4200 module.

RAK Serial Port Tool initiating at+join command

With the RAK Serial Port Tool I could get the RAK4200 connected to TTN and send unconfirmed messages. The sequence of commands I used was

at+set_config=lora:join_mode:0
at+set_config=lora:class:0
at+set_config=lora:region:AS923
at+set_config=lora:dev_eui:XXXX
at+set_config=lora:app_eui:XXXX
at+set_config=lora:app_key:XXXX
at+set_config=device:restart
at+join
at+send=lora:2:48656c6c6f204c6f526157414e

I then returned to my STM32F769I Discovery, RAK4200 Breakoutboard, Seeedstudio Grove Base Shield for Arduino and a Seeedstudio Grove-4 pin Female Jumper to Grove 4 pin Conversion Cable based test harness.

RAK4200, STM32F769I Discovery test harness

I modified the NetworkJoinOTAA sample(based on the asynchronous version of BreakOutSerial) to send the same sequence of AT commands and display the responses.

namespace devMobile.IoT.LoRaWAN.nanoFramework.RAK4200
{
   using System;
	using System.Diagnostics;
   using System.IO.Ports;
   using System.Threading;

   public class Program
	{
      private const string SerialPortId = "COM6";
      private const string DevEui = "...";
      private const string AppEui = "...";
      private const string AppKey = "...";
      private const byte MessagePort = 1;
      private const string Payload = "48656c6c6f204c6f526157414e"; // Hello LoRaWAN

      public static void Main()
      {
         string response;

         Debug.WriteLine("devMobile.IoT.Rak4200.NetworkJoinOTAA starting");

         Debug.Write("Ports:");
         foreach (string port in SerialPort.GetPortNames())
         {
            Debug.Write($" {port}");
         }
         Debug.WriteLine("");

         try
         {
            using (SerialPort serialDevice = new SerialPort(SerialPortId))
            {
               // set parameters
               serialDevice.BaudRate = 9600;
               //_SerialPort.BaudRate = 115200;
               serialDevice.Parity = Parity.None;
               serialDevice.StopBits = StopBits.One;
               serialDevice.Handshake = Handshake.None;
               serialDevice.DataBits = 8;

               serialDevice.ReadTimeout = 10000;

               serialDevice.NewLine = "\r\n";

               serialDevice.DataReceived += SerialDevice_DataReceived;

               serialDevice.Open();

               serialDevice.WatchChar = '\n';

               // clear out the RX buffer
               serialDevice.ReadExisting();
               response = serialDevice.ReadExisting();
               Debug.WriteLine($"Response :{response.Trim()} bytes:{response.Length}");
               Thread.Sleep(500);

               // Set the Working mode to LoRaWAN
               Console.WriteLine("lora:work_mode:0");
               serialDevice.WriteLine("at+set_config=lora:work_mode:0");

               // Set the JoinMode
               Console.WriteLine("lora:join_mode");
               serialDevice.WriteLine("at+set_config=lora:join_mode:0");
               Thread.Sleep(500);

               // Set the Class
               Console.WriteLine("lora:class");
               serialDevice.WriteLine("at+set_config=lora:class:0");
               Thread.Sleep(500);

               // Set the Region to AS923
               Console.WriteLine("lora:region:AS923");
               serialDevice.WriteLine("at+set_config=lora:region:AS923");
               Thread.Sleep(500);

               // Set the devEUI
               Console.WriteLine("lora:dev_eui:{DevEui}");
               serialDevice.WriteLine($"at+set_config=lora:dev_eui:{DevEui}");
               Thread.Sleep(500);

               // Set the appEUI
               Console.WriteLine("lora:app_eui:{AppEui}");
               serialDevice.WriteLine($"at+set_config=lora:app_eui:{AppEui}");
               Thread.Sleep(500);

               // Set the appKey
               Console.WriteLine("lora:app_key:{AppKey}");
               serialDevice.WriteLine($"at+set_config=lora:app_key:{AppKey}");
               Thread.Sleep(500);

               // Set the Confirm flag
               Console.WriteLine("lora:confirm:0");
               serialDevice.WriteLine("at+set_config=lora:confirm:0");
               Thread.Sleep(500);

               // Reset the device
               Console.WriteLine("device:restart");
               serialDevice.WriteLine($"at+set_config=device:restart");
               Thread.Sleep(10000);

               // Join the network
               Console.WriteLine("at+join");
               serialDevice.WriteLine("at+join");
               Thread.Sleep(10000);

               while (true)
               {
                  // Send the BCD messages
                  Console.WriteLine("lora:{MessagePort}:{Payload}");
                  serialDevice.WriteLine($"at+send=lora:{MessagePort}:{Payload}");

                  Thread.Sleep(20000);
               }
            }
         }
         catch (Exception ex)
         {
            Debug.WriteLine(ex.Message);
         }
      }

      private static void SerialDevice_DataReceived(object sender, SerialDataReceivedEventArgs e)
      {
         SerialPort serialPort = (SerialPort)sender;
         string response;

         switch (e.EventType)
         {
            case SerialData.Chars:
               break;

            case SerialData.WatchChar:
               response = serialPort.ReadExisting();
               Debug.Write(response);
               break;
            default:
               Debug.Assert(false, $"e.EventType {e.EventType} unknown");
               break;
         }
      }
   }
}

The NetworkJoinOTAA application assumes that all of the AT commands succeed

Visual Studio Output windows displaying connection process and a D2C message
TTN Console live data tab connection process
TTN Console live messaging tab C2D message

I need to find a way to set the RAK4200 back to factory settings so I can figure out what settings are persisted by the “at+set_config=device:restart” and which ones need to be set every time the application is run.

.NET nanoFramework SX127X LoRa library Refactoring

I had been planning this for a while, then the code broke when I tried to build a version for my SparkFun LoRa Gateway-1-Channel (ESP32). There was a namespace (static configuration class in configuration.cs) collision and the length of SX127XDevice.cs file was getting silly.

This refactor took a couple of days and really changed the structure of the library.

VS2022 Solution structure after refactoring

I went through the SX127XDevice.cs extracting the enumerations, masks and defaults associated with the registers the library supports.


Fork Refactoring Check-ins

The RegOpMode.cs file is a good example…

namespace devMobile.IoT.SX127xLoRaDevice
{
	using System;

	// RegOpMode bit flags from Semtech SX127X Datasheet
	[Flags]
	internal enum RegOpModeModeFlags : byte
	{
		LongRangeModeLoRa = 0b10000000,
		LongRangeModeFskOok = 0b00000000,
		LongRangeModeDefault = LongRangeModeFskOok,
		AcessSharedRegLoRa = 0b00000000,
		AcessSharedRegFsk = 0b01000000,
		AcessSharedRegDefault = AcessSharedRegLoRa,
		LowFrequencyModeOnHighFrequency = 0b00000000,
		LowFrequencyModeOnLowFrequency = 0b00001000,
		LowFrequencyModeOnDefault = LowFrequencyModeOnLowFrequency
	}

	internal enum RegOpModeMode : byte
	{
		Sleep = 0b00000000,
		StandBy = 0b00000001,
		FrequencySynthesisTX = 0b00000010,
		Transmit = 0b00000011,
		FrequencySynthesisRX = 0b00000100,
		ReceiveContinuous = 0b00000101,
		ReceiveSingle = 0b00000110,
		ChannelActivityDetection = 0b00000111,
	};
}

The library is designed to be a approximate .NET nanoFramework equivalent of Arduino-LoRa so it doesn’t support/implement all of the functionality of the Semtech SX127X. Still got a bit of refactoring to go but the structure is slowly improving.

I use Fork to manage my Github repositories, it’s an excellent product especially as it does a pretty good job of keeping me from screwing up.

.NET nanoFramework RAK4200 LoRaWAN library basic connectivity

Over the last couple of evenings I have been working on a .NET nanoFramework library for the RAKwireless RAK4200 module using a STM32F769I Discovery, RAK4200 Breakoutboard, Seeedstudio Grove Base Shield for Arduino and a Seeedstudio Grove-4 pin Female Jumper to Grove 4 pin Conversion Cable.

RAK 4200 STM32F769I Discovery testrig

My sample code has compile time options for synchronous and asynchronous operation.

//---------------------------------------------------------------------------------
// Copyright (c) May 2022, 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.
//
//---------------------------------------------------------------------------------
//#define SERIAL_SYNC_READ
#define SERIAL_ASYNC_READ
//#define SERIAL_THREADED_READ
#define ST_STM32F769I_DISCOVERY      // nanoff --target ST_STM32F769I_DISCOVERY --update 
...

namespace devMobile.IoT.LoRaWAN.nanoFramework.RAK4200
{
	using System;
	using System.Diagnostics;
	using System.IO.Ports;
	using System.Threading;

	public class Program
	{
		private static SerialPort _SerialPort;
#if SERIAL_THREADED_READ
		private static Boolean _Continue = true;
#endif
...
#if ST_STM32F769I_DISCOVERY
		private const string SerialPortId = "COM6";
#endif

		public static void Main()
		{
#if SERIAL_THREADED_READ
			Thread readThread = new Thread(SerialPortProcessor);
#endif

			Debug.WriteLine("devMobile.IoT.LoRaWAN.nanoFramework.RAK4200 BreakoutSerial starting");

			Debug.Write("Ports:");
			foreach (string port in SerialPort.GetPortNames())
			{
				Debug.Write($" {port}");
			}
			Debug.WriteLine("");

			try
			{
				// set GPIO functions for COM2 (this is UART1 on ESP32)
#if ESP32_WROOM
				Configuration.SetPinFunction(Gpio.IO04, DeviceFunction.COM2_TX);
            Configuration.SetPinFunction(Gpio.IO05, DeviceFunction.COM2_RX);
#endif

				_SerialPort = new SerialPort(SerialPortId);

				// set parameters
				_SerialPort.BaudRate = 115200;
				_SerialPort.Parity = Parity.None;
				_SerialPort.DataBits = 8;
				_SerialPort.StopBits = StopBits.One;
				_SerialPort.Handshake = Handshake.None;

				_SerialPort.ReadTimeout = 1000;
				_SerialPort.NewLine = "\r\n";

				//_SerialPort.WatchChar = '\n'; // May 2022 WatchChar event didn't fire github issue https://github.com/nanoframework/Home/issues/1035

				_SerialPort.Open();

				_SerialPort.WatchChar = '\n';

#if SERIAL_THREADED_READ
				readThread.Start();
#endif

#if SERIAL_ASYNC_READ
				_SerialPort.DataReceived += SerialDevice_DataReceived;
#endif

				while (true)
				{
					string atCommand = "at+version";
					Debug.WriteLine($"TX:{atCommand} bytes:{atCommand.Length}");
					_SerialPort.WriteLine(atCommand);

#if SERIAL_SYNC_READ
					// Read the response
					string response = _SerialPort.ReadLine();
					Debug.WriteLine($"RX:{response.Trim()} bytes:{response.Length}");
#endif
					Thread.Sleep(15000);
				}
			}
			catch (Exception ex)
			{
				Debug.WriteLine(ex.Message);
			}
		}

#if SERIAL_ASYNC_READ
		private static void SerialDevice_DataReceived(object sender, SerialDataReceivedEventArgs e)
		{
			SerialPort serialPort = (SerialPort)sender;
			string response;

			switch (e.EventType)
			{
				case SerialData.Chars:
					/*
					response = serialPort.ReadExisting();

					if ( response.Length>0)
					{ 
						Debug.WriteLine($"RX Char:{response.Trim()} bytes:{response.Length}");
					}
					*/
					break;
				case SerialData.WatchChar:
					response = serialPort.ReadExisting();

					if (response.Length > 0)
					{
						Debug.WriteLine($"RX WatchChar :{response.Trim()} bytes:{response.Length}");
					}
					break;
				default:
					Debug.Assert(false, $"e.EventType {e.EventType} unknown");
					break;
			}
		}
#endif

#if SERIAL_THREADED_READ
		public static void SerialPortProcessor()
		{
			string response;

			while (_Continue)
			{
				try
				{
					response = _SerialPort.ReadLine();
					//response = _SerialPort.ReadExisting();
					Console.WriteLine($"RX:{response} bytes:{response.Length}");
				}
				catch (TimeoutException ex) 
				{
					Console.WriteLine($"Timeout:{ex.Message}");
				}
			}
		}
#endif
	}
}

When I requested the RAK4200 Module version information with “at+version” the response was a single line (unlike the RAK3172 version where the response is three lines). The asynchronous version of the application displays character(s) as they arrive so a response could be split across multiple SerialDataReceived events.

Asynchronous approach with multiple SerialData.Chars events

With the initial version of SerialDevice_DataReceived event handler the firmware version response was available in the first SerialData.Chars event, then a SerialData.Chars event fired for each character

Asynchronous approach with SerialData.Chars events with an empty buffer removed

I also noticed that the “SerialData.WatchChar” event was not firing. After some experimentation I found that if I set the SerialPort.WatchChar before opening the serial port there were no events, but if I set SerialPort.WatchChar after opening the serial port events were fired as expected(See note re github issue in code)

Asynchronous approach with SerialPort.WatchChar work as expected

I also implemented a threaded approach for reading characters from the serial port. Normally using Exceptions for flow control is not a good idea but in this case I can’t see an alternative approach.

Thread approach SerialPort.ReadLine() timeouts

The RAK4200 Module defaults 115200 baud which seems overkill considering the throughput of a LoRaWAN link.

.NET nanoFramework SX127X LoRa library RegPaConfig RegPaDac The never ending story

While trying different my NET nanoFramework Semtech SX127X library configurations so I could explore the interactions of RegOcp(Over current protection) + RegOcpTrim I noticed something odd about the power consumption so I revisited how the output power is calculated.

Netduino3 Wifi with USB power consumption measurement

The RegPaConfig register has three settings PaSelect(RFO & PA_BOOST), MaxPower(0..7), and OutputPower(0..15). When in RFO mode the pOut has a range of -4 to 15 and PA_BOOST mode has a range of 2 to 20.

RegPaConfig register configuration options
RegPaDac register configuration options

The SX127X also has a power amplifier attached to the PA_BOOST pin and a higher power amplifier which is controlled by the RegPaDac register.

// Set RegPAConfig & RegPaDac if powerAmplifier/OutputPower settings not defaults
if ((powerAmplifier != Configuration.RegPAConfigPASelect.Default) || (outputPower != Configuration.OutputPowerDefault))
{
	if (powerAmplifier == Configuration.RegPAConfigPASelect.PABoost)
	{
		byte regPAConfigValue = (byte)Configuration.RegPAConfigPASelect.PABoost;

		// Validate the minimum and maximum PABoost outputpower
		if ((outputPower < Configuration.OutputPowerPABoostMin) || (outputPower > Configuration.OutputPowerPABoostMax))
		{
			throw new ApplicationException($"PABoost {outputPower}dBm Min power {Configuration.OutputPowerPABoostMin} to Max power {Configuration.OutputPowerPABoostMax}");
		}

		if (outputPower <= Configuration.OutputPowerPABoostPaDacThreshhold)
		{
			// outputPower 0..15 so pOut is 2=17-(15-0)...17=17-(15-15)
			regPAConfigValue |= (byte)Configuration.RegPAConfigMaxPower.Default;
			regPAConfigValue |= (byte)(outputPower - 2);

			_registerManager.WriteByte((byte)Configuration.Registers.RegPAConfig, regPAConfigValue);
			_registerManager.WriteByte((byte)Configuration.Registers.RegPaDac, (byte)Configuration.RegPaDac.Normal);
		}
		else
		{
			// outputPower 0..15 so pOut is 5=20-(15-0)...20=20-(15-15) // See https://github.com/adafruit/RadioHead/blob/master/RH_RF95.cpp around line 411 could be 23dBm
			regPAConfigValue |= (byte)Configuration.RegPAConfigMaxPower.Default;
			regPAConfigValue |= (byte)(outputPower - 5);

			_registerManager.WriteByte((byte)Configuration.Registers.RegPAConfig, regPAConfigValue);
			_registerManager.WriteByte((byte)Configuration.Registers.RegPaDac, (byte)Configuration.RegPaDac.Boost);
		}
	}
	else
	{
		byte regPAConfigValue = (byte)Configuration.RegPAConfigPASelect.Rfo;

		// Validate the minimum and maximum RFO outputPower
		if ((outputPower < Configuration.OutputPowerRfoMin) || (outputPower > Configuration.OutputPowerRfoMax))
		{
			throw new ApplicationException($"RFO {outputPower}dBm Min power {Configuration.OutputPowerRfoMin} to Max power {Configuration.OutputPowerRfoMax}");
		}

		// Set MaxPower and Power calculate pOut = PMax-(15-outputPower), pMax=10.8 + 0.6*MaxPower 
		if (outputPower > Configuration.OutputPowerRfoThreshhold)
		{
			// pMax 15=10.8+0.6*7 with outputPower 0...15 so pOut is 15=pMax-(15-0)...0=pMax-(15-15) 
			regPAConfigValue |= (byte)Configuration.RegPAConfigMaxPower.Max;
			regPAConfigValue |= (byte)(outputPower + 0);
		}
		else
		{
			// pMax 10.8=10.8+0.6*0 with output power 0..15 so pOut is -4=10-(15-0)...10.8=10.8-(15-15)
			regPAConfigValue |= (byte)Configuration.RegPAConfigMaxPower.Min;
			regPAConfigValue |= (byte)(outputPower + 4);
		}

		_registerManager.WriteByte((byte)Configuration.Registers.RegPAConfig, regPAConfigValue);
		_registerManager.WriteByte((byte)Configuration.Registers.RegPaDac, (byte)Configuration.RegPaDac.Normal);
	}
}

// Set RegOcp if any of the settings not defaults
if ((ocpOn != Configuration.RegOcp.Default) || (ocpTrim != Configuration.RegOcpTrim.Default))
{
	byte regOcpValue = (byte)ocpTrim;

	regOcpValue |= (byte)ocpOn;

	_registerManager.WriteByte((byte)Configuration.Registers.RegOcp, regOcpValue);
}

After reviewing the code I realised that the the RegPaDac test around line 14 should <= rather than <

.NET nanoFramework SX127X LoRa library RegLna LnaGain

Every so often I print my code out (landscape for notes in margin, double sided to save paper, and colour so it looks like Visual Studio 2022) and within 100 lines noticed the first of no doubt many issues. The SX127X RegLNA enumeration was wrong.

// RegLna
[Flags]
public enum RegLnaLnaGain : byte
{
	G1 = 0b00000001,
	G2 = 0b00000010,
	G3 = 0b00000011,
	G4 = 0b00000100,
	G5 = 0b00000101,
	G6 = 0b00000110
}
SX127X RegLna options

The LnaGain value is bits 5-7 rather than rather than bits 0-2 which could be a problem if the specified lnaGain and lnaBoost values are not the default values.

// Set RegLna if any of the settings not defaults
if ((lnaGain != Configuration.LnaGainDefault) || (lnaBoost != Configuration.LnaBoostDefault))
{
	byte regLnaValue = (byte)lnaGain;

	regLnaValue |= Configuration.RegLnaLnaBoostLfDefault;
	regLnaValue |= Configuration.RegLnaLnaBoostHfDefault;

	if (lnaBoost)
	{
		if (_frequency > Configuration.SX127XMidBandThreshold)
		{
			regLnaValue |= Configuration.RegLnaLnaBoostHfOn;
		}
		else
		{
			regLnaValue |= Configuration.RegLnaLnaBoostLfOn;
		}
	}
	_registerManager.WriteByte((byte)Configuration.Registers.RegLna, regLnaValue);
}

The default lnaGain is G1 and the default lnaBoost is false so if the gain was set to G3(011) then LnaBoostHf current would be 150% and LnaGain would be 000 which is a reserved value.

// RegLna
[Flags]
public enum RegLnaLnaGain : byte
{
	G1 = 0b00100000,
	G2 = 0b01000000,
	G3 = 0b01100000,
	G4 = 0b10000000,
	G5 = 0b10100000,
	G6 = 0b11000000
}

I need to check my usage of Configuration.SX127XMidBandThreshold for LnaBoostLf vs. LnaBoostHf is correct.(arduino-LoRa)

.NET nanoFramework SX127X LoRa library “it’s all about timing”

Every so often my nanoFramework SX127X library RangeTester application wouldn’t start. When I poked around with the Visual Studio 2022 debugger the issue went away(a “Heisenbug” in the wild) which made figuring out what was going on impossible.

One afternoon the issue occurred several times in a row, the application wouldn’t startup because the SX127X device detection failed and message transmission was also not being confirmed.(TX Done).

Visual Studio output windows with SX127X detection failure
Visual Studio output windows with no Transmit confirmations
public SX127XDevice(SpiDevice spiDevice, GpioController gpioController, int interruptPin, int resetPin)
{
	_gpioController = gpioController;

	// Factory reset pin configuration
	_resetPin = resetPin;
	_gpioController.OpenPin(resetPin, PinMode.Output);

	_gpioController.Write(resetPin, PinValue.Low);
	Thread.Sleep(20);
	_gpioController.Write(resetPin, PinValue.High);
	Thread.Sleep(100);

	_registerManager = new RegisterManager(spiDevice, RegisterAddressReadMask, RegisterAddressWriteMask);

	// Once the pins setup check that SX127X chip is present
	Byte regVersionValue = _registerManager.ReadByte((byte)Configuration.Registers.RegVersion);
	if (regVersionValue != Configuration.RegVersionValueExpected)
	{
		throw new ApplicationException("Semtech SX127X not found");
	}

	// Interrupt pin for RX message & TX done notification 
	_gpioController.OpenPin(interruptPin, PinMode.InputPullDown);

	_gpioController.RegisterCallbackForPinValueChangedEvent(interruptPin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
}

I could single step through the code and inspect variables with the debugger and it looks like a timing issue with order of the strobing of the reset pin and the initialisation of the RegisterManager. I’ll spend and hour starting and stopping the application, then smoke test the code for 24 hours with a couple of other devices generating traffic just to check.

.NET nanoFramework SX127X LoRa library playing nice with others

So nanoFramework applications using my SX127X library.NetNF can access other General Purpose Input Output(GPIO) ports and Serial Peripheral Interface(SPI) devices I have added SpiDevice and GpioController parameters to the two constructors.

// Hardware configuration support
private readonly int ResetPin;
private readonly GpioController _gpioController = null;
private readonly SpiDevice _sx127xTransceiver = null;
private readonly Object SX127XRegFifoLock = new object();
private double Frequency = FrequencyDefault;
private bool RxDoneIgnoreIfCrcMissing = true;
private bool RxDoneIgnoreIfCrcInvalid = true;

public SX127XDevice(SpiDevice spiDevice, GpioController gpioController, int interruptPin, int resetPin)
{
	_sx127xTransceiver = spiDevice;

	_gpioController = gpioController;

	// As soon as ChipSelectLine/ChipSelectLogicalPinNumber check that SX127X chip is present
	Byte regVersionValue = this.ReadByte((byte)Registers.RegVersion);
	if (regVersionValue != RegVersionValueExpected)
	{
		throw new ApplicationException("Semtech SX127X not found");
	}

	// Factory reset pin configuration
	ResetPin = resetPin;
	_gpioController.OpenPin(resetPin, PinMode.Output);

	_gpioController.Write(resetPin, PinValue.Low);
	Thread.Sleep(20);
	_gpioController.Write(resetPin, PinValue.High);
	Thread.Sleep(20);

	// Interrupt pin for RX message & TX done notification 
	_gpioController.OpenPin(interruptPin, PinMode.InputPullDown);

	_gpioController.RegisterCallbackForPinValueChangedEvent(interruptPin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
}

public SX127XDevice(SpiDevice spiDevice, GpioController gpioController, int interruptPin)
{
	_sx127xTransceiver = spiDevice;

	_gpioController = gpioController;

	// As soon as ChipSelectLine/ChipSelectLogicalPinNumber check that SX127X chip is present
	Byte regVersionValue = this.ReadByte((byte)Registers.RegVersion);
	if (regVersionValue != RegVersionValueExpected)
	{
		throw new ApplicationException("Semtech SX127X not found");
	}

	// Interrupt pin for RX message & TX done notification 
	_gpioController.OpenPin(interruptPin, PinMode.InputPullDown);

	_gpioController.RegisterCallbackForPinValueChangedEvent(interruptPin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
}

I then “over refactored”(broke) the constructor without the resetPin by removing the GpioController parameter which is necessary for the RegisterCallbackForPinValueChangedEvent.

.NET nanoFramework SX127X LoRa library on Github

The source code of my nanoFramework SX127X library is now available on GitHub. I have tested the library and sample applications on Netduino 3Wifi, Sparkfun LoRa Gateway 1 Channel ESP32 for LoRaWAN and ST Micro STM32F7691 Discovery devices.(I can add more platform configurations if there is interest).

STM32F769I Discovery, Netduino 3 Wifi and Sparkfun testrig

I started with a proof of concept update of my RFM9X for nanoFramework library to the new nanoFramework System.Device model (“inspired” by .Net Core System.Device) which was slow going. I then tried “back porting” my SX127X for .Net Core library to the .NET nanoFramework which was much quicker.

namespace devMobile.IoT.SX127xLoRaDevice
{
	using System;
	using System.Text;
	using System.Threading;

	class Program
	{
		private const double Frequency = 915000000.0;
#if ESP32_WROOM_32_LORA_1_CHANNEL
      private const int SpiBusId = 1;
#endif
#if NETDUINO3_WIFI
		private const int SpiBusId = 2;
#endif
#if ST_STM32F769I_DISCOVERY
		private const int SpiBusId = 2;
#endif
		private static SX127XDevice sx127XDevice;

		static void Main(string[] args)
		{
			int SendCount = 0;
#if ESP32_WROOM_32_LORA_1_CHANNEL // No reset line for this device as it isn't connected on SX127X
			int chipSelectLine = Gpio.IO16;
			int interruptPinNumber = Gpio.IO26;
#endif
#if NETDUINO3_WIFI
			// Arduino D10->PB10
			int chipSelectLine = PinNumber('B', 10);
			// Arduino D9->PE5
			int resetPinNumber = PinNumber('E', 5);
			// Arduino D2 -PA3
			int interruptPinNumber = PinNumber('A', 3);
#endif
#if ST_STM32F769I_DISCOVERY
			// Arduino D10->PA11
			int chipSelectLine = PinNumber('A', 11);
			// Arduino D9->PH6
			int resetPinNumber = PinNumber('H', 6);
			// Arduino D2->PA4
			int interruptPinNumber = PinNumber('J', 1);
#endif
			Console.WriteLine("devMobile.IoT.SX127xLoRaDevice Client starting");

			try
			{
#if ESP32_WROOM_32_LORA_1_CHANNEL
				Configuration.SetPinFunction(Gpio.IO12, DeviceFunction.SPI1_MISO);
				Configuration.SetPinFunction(Gpio.IO13, DeviceFunction.SPI1_MOSI);
				Configuration.SetPinFunction(Gpio.IO14, DeviceFunction.SPI1_CLOCK);

				sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine, interruptPinNumber);
#endif
#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
				sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine, interruptPinNumber, resetPinNumber);
#endif

				sx127XDevice.Initialise(SX127XDevice.RegOpModeMode.ReceiveContinuous,
							Frequency,
							lnaGain: SX127XDevice.RegLnaLnaGain.G3,
							lnaBoost:true, 
							powerAmplifier: SX127XDevice.PowerAmplifier.PABoost,
							rxPayloadCrcOn: true,
							rxDoneignoreIfCrcMissing: false
							);

#if DEBUG
				sx127XDevice.RegisterDump();
#endif

				sx127XDevice.OnReceive += SX127XDevice_OnReceive;
				sx127XDevice.Receive();
				sx127XDevice.OnTransmit += SX127XDevice_OnTransmit;

				Thread.Sleep(500);

				while (true)
				{
					string messageText = $"Hello LoRa from .NET nanoFramework {SendCount += 1}!";

					byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
					//Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-TX {messageBytes.Length} byte message {messageText}");
					//sx127XDevice.Send(messageBytes);

					Thread.Sleep(50000);
				}
			}
			catch (Exception ex)
			{
				Console.WriteLine(ex.Message);
			}
		}

		private static void SX127XDevice_OnReceive(object sender, SX127XDevice.OnDataReceivedEventArgs e)
		{
			try
			{
				// Remove unprintable characters from messages
				for (int index = 0; index < e.Data.Length; index++)
				{
					if ((e.Data[index] < 0x20) || (e.Data[index] > 0x7E))
					{
						e.Data[index] = 0x7C;
					}
				}

				string messageText = UTF8Encoding.UTF8.GetString(e.Data, 0, e.Data.Length);

				Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-RX PacketSnr {e.PacketSnr:0.0} Packet RSSI {e.PacketRssi}dBm RSSI {e.Rssi}dBm = {e.Data.Length} byte message {messageText}");
			}
			catch (Exception ex)
			{
				Console.WriteLine(ex.Message);
			}
		}

		private static void SX127XDevice_OnTransmit(object sender, SX127XDevice.OnDataTransmitedEventArgs e)
		{
			sx127XDevice.SetMode(SX127XDevice.RegOpModeMode.ReceiveContinuous);

			Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-TX Done");
		}

#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
		static int PinNumber(char port, byte pin)
		{
			if (port < 'A' || port > 'J')
				throw new ArgumentException();

			return ((port - 'A') * 16) + pin;
		}
#endif
	}
}

The sample application shows how to configure the library for different devices (SPI port, interrupt pin and optional reset pin) then send/receive payloads. The library is intended to be initialised then run for long periods of time (I’m looking at a month long soak test next) rather than changing configuration while running. The initialise method has many parameters which have “reasonable” default values. (Posts coming about optimising power consumption and range).

I’m looking at extending the library with optional functionality like tamper detection via signing and privacy via payload encryption, and mesh network support.

.NET nanoFramework SX127X LoRa library with Interrupts

To test the nanoFramework transmit and receive with interrupts implementation I used three Dragino LoRa Shields, a Seeeduino V4.2 and a pair of Netduino 3 Wifi devices.

Seeeduino and nanoFramework

I started with transmit as I was confident my Netduino 3 Wifi & Seeeduino + Dragino LoRa Shields could receive messages.

Interrupt pin configuration
SX127X ReqIrqFlags options

The TransmitInterrupt application loads the message to be sent into the First In First Out(FIFO) buffer, RegDioMapping1 is set to interrupt onTxDone(PacketSent-00), then RegRegOpMode-Mode is set to Transmit. When the message has been sent InterruptGpioPin_ValueChanged is called, and the TxDone(0b00001000) flag is set in the RegIrqFlags register.

The ReceiveInterrupt application sets the RegDioMapping1 to interrupt on RxDone(PacketReady-00), then the RegRegOpMode-Mode is set to Receive(TX-101). When a message is received InterruptGpioPin_ValueChanged is called, with the RxDone(0b00001000) flag set in the RegIrqFlags register, and then the message is read from First In First Out(FIFO) buffer.

namespace devMobile.IoT.SX127x.ReceiveTransmitInterrupt
{
...
   public sealed class SX127XDevice
   {
...
      public SX127XDevice(int busId, int chipSelectLine, int interruptPin, int resetPin)
      {
         var settings = new SpiConnectionSettings(busId, chipSelectLine)
         {
            ClockFrequency = 1000000,
            Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
            SharingMode = SpiSharingMode.Shared
         };

         SX127XTransceiver = new SpiDevice(settings);

         GpioController gpioController = new GpioController();


         // Factory reset pin configuration
         gpioController.OpenPin(resetPin, PinMode.Output);

         gpioController.Write(resetPin, PinValue.Low);
         Thread.Sleep(20);
         gpioController.Write(resetPin, PinValue.High);
         Thread.Sleep(20);

         // Interrupt pin for RX message & TX done notification 
         gpioController.OpenPin(interruptPin, PinMode.InputPullDown);

         gpioController.RegisterCallbackForPinValueChangedEvent(interruptPin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
      }
...
   }

      private void InterruptGpioPin_ValueChanged(object sender, PinValueChangedEventArgs e)
      {
         byte irqFlags = this.ReadByte(0x12); // RegIrqFlags
         Debug.WriteLine($"RegIrqFlags 0X{irqFlags:x2}");

         if ((irqFlags & 0b01000000) == 0b01000000)  // RxDone 
         {
            Debug.WriteLine("Receive-Message");
            byte currentFifoAddress = this.ReadByte(0x10); // RegFifiRxCurrent
            this.WriteByte(0x0d, currentFifoAddress); // RegFifoAddrPtr

            byte numberOfBytes = this.ReadByte(0x13); // RegRxNbBytes

            // Allocate buffer for message
            byte[] messageBytes = this.ReadBytes(0X0, numberOfBytes);

            // Remove unprintable characters from messages
            for (int index = 0; index < messageBytes.Length; index++)
            {
               if ((messageBytes[index] < 0x20) || (messageBytes[index] > 0x7E))
               {
                  messageBytes[index] = 0x20;
               }
            }

            string messageText = UTF8Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length);
            Debug.WriteLine($"Received {messageBytes.Length} byte message {messageText}");
         }

         if ((irqFlags & 0b00001000) == 0b00001000)  // TxDone
         {
            this.WriteByte(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous
            Debug.WriteLine("Transmit-Done");
         }

         this.WriteByte(0x40, 0b00000000); // RegDioMapping1 0b00000000 DI0 RxReady & TxReady
         this.WriteByte(0x12, 0xff);// RegIrqFlags
      }

   public class Program
   {
...
   #if NETDUINO3_WIFI
      private const int SpiBusId = 2;
#endif
...

      public static void Main()
      {
         int SendCount = 0;
...
#if NETDUINO3_WIFI
         // Arduino D10->PB10
         int chipSelectLine = PinNumber('B', 10);
         // Arduino D9->PE5
         int resetPinNumber = PinNumber('E', 5);
         // Arduino D2 -PA3
         int interruptPinNumber = PinNumber('A', 3);
#endif
...
  
       Debug.WriteLine("devMobile.IoT.SX127x.ReceiveTransmitInterrupt starting");

         try
         {
...
#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
            SX127XDevice sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine, interruptPinNumber, resetPinNumber);
#endif
            Thread.Sleep(500);

            // Put device into LoRa + Sleep mode
            sx127XDevice.WriteByte(0x01, 0b10000000); // RegOpMode 

            // Set the frequency to 915MHz
            byte[] frequencyWriteBytes = { 0xE4, 0xC0, 0x00 }; // RegFrMsb, RegFrMid, RegFrLsb
            sx127XDevice.WriteBytes(0x06, frequencyWriteBytes);

            // More power PA Boost
            sx127XDevice.WriteByte(0x09, 0b10000000); // RegPaConfig

            sx127XDevice.WriteByte(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous

            while (true)
            {
               // Set the Register Fifo address pointer
               sx127XDevice.WriteByte(0x0E, 0x00); // RegFifoTxBaseAddress 

               // Set the Register Fifo address pointer
               sx127XDevice.WriteByte(0x0D, 0x0); // RegFifoAddrPtr 

               string messageText = $"Hello LoRa {SendCount += 1}!";

               // load the message into the fifo
               byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
               sx127XDevice.WriteBytes(0x0, messageBytes); // RegFifo 

               // Set the length of the message in the fifo
               sx127XDevice.WriteByte(0x22, (byte)messageBytes.Length); // RegPayloadLength
               sx127XDevice.WriteByte(0x40, 0b01000000); // RegDioMapping1 0b00000000 DI0 RxReady & TxReady
               sx127XDevice.WriteByte(0x01, 0b10000011); // RegOpMode 

               Debug.WriteLine($"Sending {messageBytes.Length} bytes message {messageText}");

               Thread.Sleep(10000);
            }
         }
         catch (Exception ex)
         {
            Debug.WriteLine(ex.Message);
         }
      }
...
   }
}
ReceiveTransmitInterrupt application output

The ReceiveTransmitInterrupt application combines the functionality TransmitInterrupt and ReceiveInterrupt programs. The key differences are the RegDioMapping1 setup and in InterruptGpioPin_ValueChanged where the TxDone & RxDone flags in the RegIrqFlags register specify how the interrupt is handled.

.NET nanoFramework SX127X LoRa library Basic Receive & Transmit

For testing nanoFramework device transmit and receive functionality I used an Arduino/Seeeduino with a Dragino LoRa Shield (running one of the Arduino-LoRa samples) as a client device. This was so I could “bootstrap” connectivity and test interoperability with other libraries/platforms.

Arduino/Netduino devices for .NET nanoFramework interoperability test-rig

I started with transmit as I was confident my Seeeduino + Dragino LoRa Shield could receive messages. The TransmitBasic application puts the device into LoRa + Sleep mode as after reset/powering up the device is in FSK/OOK, Low Frequency + Standby mode).

SX127X RegOpMode options

After loading the message to be sent into the First In First Out(FIFO) buffer, the RegOpMode-Mode is set to Transmit(TX-011), and then the RegIrqFlags register is polled until the TxDone flag is set.

SX127X ReqIrqFlags options
public static void Main()
{
  int SendCount = 0;
...
  Debug.WriteLine("devMobile.IoT.SX127x.TransmitBasic starting");

   try
   {
...
#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
      SX127XDevice sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine, resetPinNumber);
#endif
     Thread.Sleep(500);

     // Put device into LoRa + Standby mode
     sx127XDevice.WriteByte(0x01, 0b10000000); // RegOpMode 

      // Set the frequency to 915MHz
      byte[] frequencyBytes = { 0xE4, 0xC0, 0x00 }; // RegFrMsb, RegFrMid, RegFrLsb
      sx127XDevice.WriteBytes(0x06, frequencyBytes);

      // More power PA Boost
      sx127XDevice.WriteByte(0x09, 0b10000000); // RegPaConfig

      sx127XDevice.RegisterDump();

      while (true)
      {
         sx127XDevice.WriteByte(0x0E, 0x0); // RegFifoTxBaseAddress 

         // Set the Register Fifo address pointer
         sx127XDevice.WriteByte(0x0D, 0x0); // RegFifoAddrPtr 

         string messageText = $"Hello LoRa from .NET nanoFramework {SendCount += 1}!";

         // load the message into the fifo
         byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
         sx127XDevice.WriteBytes(0x0, messageBytes); // RegFifo

         // Set the length of the message in the fifo
         sx127XDevice.WriteByte(0x22, (byte)messageBytes.Length); // RegPayloadLength

         Debug.WriteLine($"Sending {messageBytes.Length} bytes message {messageText}");
         // Set the mode to LoRa + Transmit
         sx127XDevice.WriteByte(0x01, 0b10000011); // RegOpMode 

         // Wait until send done, no timeouts in PoC
         Debug.WriteLine("Send-wait");
         byte irqFlags = sx127XDevice.ReadByte(0x12); // RegIrqFlags
         while ((irqFlags & 0b00001000) == 0)  // wait until TxDone cleared
         {
            Thread.Sleep(10);
            irqFlags = sx127XDevice.ReadByte(0x12); // RegIrqFlags
            Debug.Write(".");
         }
         Debug.WriteLine("");
         sx127XDevice.WriteByte(0x12, 0b00001000); // clear TxDone bit
         Debug.WriteLine("Send-Done");

         Thread.Sleep(30000);
         }
      }
      catch (Exception ex)
      {
         Debug.WriteLine(ex.Message);
      }
   }
}
Transmit Basic application output

Once the TransmitBasic application was sending messages reliably I started working on the ReceiveBasic application. As the ReceiveBasic application starts up the SX127X RegOpMode has to be set to sleep/standby so the device can be configured. TOnce that is completed RegOpMode-Mode is set to RxContinuous(101), and the RegIrqFlags register is polled until the RxDone flag is set.

public static void Main()
{
...
   Debug.WriteLine("devMobile.IoT.SX127x.ReceiveBasic starting");

   try
   {
...
#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
      SX127XDevice sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine, resetPinNumber);
#endif
      Thread.Sleep(500);

      // Put device into LoRa + Sleep mode
      sx127XDevice.WriteByte(0x01, 0b10000000); // RegOpMode 

      // Set the frequency to 915MHz
      byte[] frequencyBytes = { 0xE4, 0xC0, 0x00 }; // RegFrMsb, RegFrMid, RegFrLsb
      sx127XDevice.WriteBytes(0x06, frequencyBytes);

      sx127XDevice.WriteByte(0x0F, 0x0); // RegFifoRxBaseAddress 

      sx127XDevice.WriteByte(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous

       while (true)
       {
          // Wait until a packet is received, no timeouts in PoC
         Debug.WriteLine("Receive-Wait");
         byte irqFlags = sx127XDevice.ReadByte(0x12); // RegIrqFlags
         while ((irqFlags & 0b01000000) == 0)  // wait until RxDone cleared
         {
            Thread.Sleep(100);
            irqFlags = sx127XDevice.ReadByte(0x12); // RegIrqFlags
            Debug.Write(".");
         }
         Debug.WriteLine("");
         Debug.WriteLine($"RegIrqFlags 0X{irqFlags:X2}");
         Debug.WriteLine("Receive-Message");
         byte currentFifoAddress = sx127XDevice.ReadByte(0x10); // RegFifiRxCurrent
         sx127XDevice.WriteByte(0x0d, currentFifoAddress); // RegFifoAddrPtr

         byte numberOfBytes = sx127XDevice.ReadByte(0x13); // RegRxNbBytes

         // Read the message from the FIFO
         byte[] messageBytes = sx127XDevice.ReadBytes(0x00, numberOfBytes);

         sx127XDevice.WriteByte(0x0d, 0);
         sx127XDevice.WriteByte(0x12, 0b11111111); // RegIrqFlags clear all the bits

         // Remove unprintable characters from messages
         for (int index = 0; index < messageBytes.Length; index++)
         {
            if ((messageBytes[index] < 0x20) || (messageBytes[index] > 0x7E))
            {
               messageBytes[index] = 0x20;
            }
         }

         string messageText = UTF8Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length);
         Debug.WriteLine($"Received {messageBytes.Length} byte message {messageText}");

         Debug.WriteLine("Receive-Done");
      }
    }
   catch (Exception ex)
   {
      Debug.WriteLine(ex.Message);
   }
}
Receive Basic application output

Every so often the ReceiveBasic application would display a message sent on the same frequency by a device somewhere nearby.

ReceiveBasic application messages from unknown source

I need to do some more investigation into whether writing 0b00001000 (Transmit) vs. 0b11111111(Receive) to RegIrqFlags is important.