.NET nanoFramework RAK2305 – RAK4200 Library Usage (AS923 Sorted)

This post covers the usage of my RAK4200LoRaWAN-NetNF library with a RAK3205 WisBlock Wifi Interface Module on a RAK4200 Evaluation Board. This post was delayed because of the issue covered in .NET nanoFramework RAK2305 – RAK4200 Library Usage AS923 Issue. After posting in the RAKWireless formus RAKWireless support very quickly provided updated RAK4200 firmware which fixed the issue.

RAK2305 RAK4200 Evaluation Board based test rig

The RAK4200LoRaWANDeviceClient now reliably joins The Things Network, then sends and receives messages.

When I initially deployed the RAK4200LoRaWANDeviceClient the RAK4200LoRaWAN-NetNF library failed in the OtaaInitialise method. I think this was caused by the “at+set_config=lora:work_mode:0” command rebooting the RAK4200 Module. I have commented out the code but may move it to a standalone method if required.

// Set the Working mode to LoRaWAN, not/never going todo P2P with this library.
#if DIAGNOSTICS
Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} at+set_config=lora:work_mode:0");
#endif
Result result = SendCommand("Initialization OK", "at+set_config=lora:work_mode:0", CommandTimeoutDefault);
if (result != Result.Success)
{
#if DIAGNOSTICS
         Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} at+set_config=lora:work_mode:0 failed {result}");
#endif
	return result;
}

I think it would be reasonable to assume that the device is in the correct mode (the default after a reset to factory) on startup so I removed the LoRa® network work mode configuration code.

.NET nanoFramework RAK2305 – RAK4200 Library AS923 Issue

This post was going to be about how to the use my RAK4200LoRaWAN-NetNF library with a RAK3205 WisBlock Wifi Interface Module and RAK4200 Evaluation Board but there was a problem…

RAK2305 RAK4200 Evaluation Board based test rig

When I ran the RAK4200LoRaWANDeviceClient the first couple of join attempts failed which was odd as my sparkfun ESP32 thing plus with RAK4200 Breakout Board setup was very reliable.

Visual Studio Debug output for RAK4200LoRaWANDeviceClient Join failure
The Things Network RAK4200LoRaWANDeviceClient application Join failure

When I looked at The Things Network “Live data” tab the RAK4200 Module on the RAK4200 Evaluation Board wasn’t using the LoRaWAN AS923 Join-Request channels 923.20 & 923.40 MHz.

AS923 Join Channels

The RAK4200 Module on the appeared to be cycling through all the AS923 channels and every so often would use one the join request channels.

Visual Studio Debug output for RAK4200LoRaWANDeviceClient successful Join and Send
The Things Network RAK4200LoRaWANDeviceClient successful Join and Send

The RAK4200 Breakout Board module is running a later firmware version (V3.2.0.16) than the RAK4200 Evaluation Board module (V3.2.0.15) which is most probably the problem.

Visual Studio Debug output for RAK4200 Evaluation Board Version Request
Visual Studio Debug output for RAK4200 Breakout Board Version Request

The RAK811 module (which has been retired) also had similar issues with AS923.

.NET nanoFramework RAK2305 – RAK3172 Library Usage

This post covers the usage of my RAK3172LoRaWAN-NetNF library with a RAK3205 WisBlock Wifi Interface Module on a RAK3172 Evaluation Board.

RAK2305 RAK3172 Evaluation Board based test rig

The first time the RAK3172LoRaWANDeviceClient is run the following preprocessor directives may need to be defined to configure the RAK3172 module.

//---------------------------------------------------------------------------------
//#define ST_STM32F769I_DISCOVERY      // nanoff --target ST_STM32F769I_DISCOVERY --update 
//#define  SPARKFUN_ESP32_THING_PLUS  // nanoff --platform esp32 --serialport COM4 --update
//#define RAK_WISBLOCK_RAK2305 // nanoff --update --target ESP32_PSRAM_REV0 --serialport COM4
#define DEVICE_DEVEUI_SET
//#define FACTORY_RESET
//#define PAYLOAD_BCD
#define PAYLOAD_BYTES
#define OTAA
//#define ABP
//#define CONFIRMED
#define UNCONFIRMED
#define REGION_SET
#define ADR_SET
//#define SLEEP
namespace devMobile.IoT.LoRaWAN
{
Visual Studio Debug output for RAK3172LoRaWANDeviceClient full configuration

Once the RAK3172 Module is the RAK3172LoRaWANDeviceClient can be run with only PAYLOAD_BCD or PAYLOAD_BYTES defined

//---------------------------------------------------------------------------------
//#define ST_STM32F769I_DISCOVERY      // nanoff --target ST_STM32F769I_DISCOVERY --update 
//#define  SPARKFUN_ESP32_THING_PLUS  // nanoff --platform esp32 --serialport COM4 --update
//#define RAK_WISBLOCK_RAK2305 // nanoff --update --target ESP32_PSRAM_REV0 --serialport COM4
//#define DEVICE_DEVEUI_SET
//#define FACTORY_RESET
//#define PAYLOAD_BCD
#define PAYLOAD_BYTES
//#define OTAA
//#define ABP
//#define CONFIRMED
//#define UNCONFIRMED
//#define REGION_SET
//#define ADR_SET
//#define SLEEP
namespace devMobile.IoT.LoRaWAN
{
Visual Studio Debug output for RAK3172LoRaWANDeviceClient minimal configuration

When I initially deployed ran the RAK3172LoRaWANDeviceClient the RAK3172LoRaWAN-NetNF library crashed in the OtaaInitialise method. I think this was caused by the RAKwireless Unified Interface V3(RUIV3) “AT+NWM=1” command rebooting the RAK3172 Module.

// Set the Working mode to LoRaWAN, not/never going todo P2P with this library.
#if DIAGNOSTICS
Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} AT+NWM=1");
#endif
Result result = SendCommand("Current Work Mode: LoRaWAN.", "AT+NWM=1", CommandTimeoutDefault);
if (result != Result.Success)
{
#if DIAGNOSTICS
	Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} AT+NWM=1 failed {result}");
#endif
	return result;
}

I think it would be reasonable to assume that the device is in the correct mode (the default after a reset to factory) on startup so I removed the LoRa® network work mode configuration code.

.NET nanoFramework RAK3172 Library Usage

After a two week “soak test” using a Sparkfun Thing Plus ESP32 WROOM and RAK3172 Breakout Board completed with no failures, this final post covers the usage of the RAK3172LoRaWAN-NetNF library in a “real-world” application.

Before a factory reset the DevEUI, JoinEUI (was AppEUI), and AppKey were values I had configured earlier

12:02:04 0 TX:AT+DEVEUI=? bytes:11--------------------------------
AT+DEVEUI=A..............1
OK

12:03:05 0 TX:AT+APPEUI=? bytes:11--------------------------------
AT+APPEUI=A..............8
OK

12:04:03 0 TX:AT+APPKEY=? bytes:11--------------------------------
AT+APPKEY=C..............................F
OK

After a factory reset the DevEUI, JoinEUI (was AppEUI), and AppKey were default values

12:00:21 0 TX:AT+DEVEUI=? bytes:11--------------------------------
AT+DEVEUI=0000000000000000
OK

12:01:09 0 TX:AT+APPEUI=? bytes:11--------------------------------
AT+APPEUI=0000000000000000
OK

12:01:48 0 TX:AT+APPKEY=? bytes:11--------------------------------
AT+APPKEY=00000000000000000000000000000000
OK

I then ran the RAK3172LoRaWANDeviceClient with the following preprocessor directives defined to reconfigure the RAK3172 module.

//---------------------------------------------------------------------------------
//#define ST_STM32F769I_DISCOVERY      // nanoff --target ST_STM32F769I_DISCOVERY --update 
#define ESP32_WROOM   // nanoff --target ESP32_REV0 --serialport COM17 --update
#define DEVICE_DEVEUI_SET
//#define FACTORY_RESET
///#define PAYLOAD_BCD
#define PAYLOAD_BYTES
#define OTAA
//#define ABP
//#define CONFIRMED
#define UNCONFIRMED
#define REGION_SET
#define ADR_SET
//#define SLEEP
namespace devMobile.IoT.LoRaWAN
{
...
Visual Studio Debug output for RAK3172LoRaWANDeviceClient full configuration

I could then run the RAK3172LoRaWANDeviceClient with only PAYLOAD_BCD or PAYLOAD_BYTES defined

//---------------------------------------------------------------------------------
//#define ST_STM32F769I_DISCOVERY      // nanoff --target ST_STM32F769I_DISCOVERY --update 
#define ESP32_WROOM   // nanoff --target ESP32_REV0 --serialport COM17 --update
//#define DEVICE_DEVEUI_SET
//#define FACTORY_RESET
///#define PAYLOAD_BCD
#define PAYLOAD_BYTES
//#define OTAA
//#define ABP
//#define CONFIRMED
//#define UNCONFIRMED
//#define REGION_SET
//#define ADR_SET
//#define SLEEP
namespace devMobile.IoT.LoRaWAN
{
...
Visual Studio Debug output for RAK3172LoRaWANDeviceClient minimal configuration
public static void Main()
{
	Result result;

	Debug.WriteLine("devMobile.IoT.RAK3172LoRaWANDeviceClient starting");

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

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

		using (Rak3172LoRaWanDevice device = new Rak3172LoRaWanDevice())
		{
			result = device.Initialise(SerialPortId, 115200, Parity.None, 8, StopBits.One);
			if (result != Result.Success)
			{
				Debug.WriteLine($"Initialise failed {result}");
				return;
			}

			MessageSendTimer = new Timer(SendMessageTimerCallback, device, Timeout.Infinite, Timeout.Infinite);
					
			device.OnJoinCompletion += OnJoinCompletionHandler;
			device.OnReceiveMessage += OnReceiveMessageHandler;
#if CONFIRMED
			device.OnMessageConfirmation += OnMessageConfirmationHandler;
#endif

#if FACTORY_RESET
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} FactoryReset");
			result = device.FactoryReset();
			if (result != Result.Success)
			{
				Debug.WriteLine($"FactoryReset failed {result}");
				return;
			}
#endif

#if DEVICE_DEVEUI_SET
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Device EUI");
			result = device.DeviceEui(Config.devEui);
			if (result != Result.Success)
			{
				Debug.WriteLine($"DeviceEUI set failed {result}");
				return;
			}
#endif

#if REGION_SET
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Region{Band}");
			result = device.Band(Band);
			if (result != Result.Success)
			{
				Debug.WriteLine($"Band on failed {result}");
				return;
			}
#endif

#if ADR_SET
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} ADR On");
			result = device.AdrOn();
			if (result != Result.Success)
			{
				Debug.WriteLine($"ADR on failed {result}");
				return;
			}
#endif
#if CONFIRMED
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Confirmed");
			result = device.UplinkMessageConfirmationOn();
			if (result != Result.Success)
			{
				Debug.WriteLine($"Confirm on failed {result}");
				return;
			}
#endif
#if UNCONFIRMED
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Unconfirmed");
			result = device.UplinkMessageConfirmationOff();
			if (result != Result.Success)
			{
				Debug.WriteLine($"Confirm off failed {result}");
				return;
			}
#endif

#if OTAA
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} OTAA");
			result = device.OtaaInitialise(Config.JoinEui, Config.AppKey);
			if (result != Result.Success)
			{
				Debug.WriteLine($"OTAA Initialise failed {result}");
				return;
			}
#endif

#if ABP
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} ABP");
			result = device.AbpInitialise(Config.DevAddress, Config.NwksKey, Config.AppsKey);
			if (result != Result.Success)
			{
				Debug.WriteLine($"ABP Initialise failed {result}");
				return;
			}
#endif

			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Join start Timeout:{JoinTimeOut:hh:mm:ss}");
			result = device.Join(JoinTimeOut);
			if (result != Result.Success)
			{
				Debug.WriteLine($"Join failed {result}");
				return;
			}
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Join started");

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

One of the major differences between the RAK4200 and RAK3127 libraries is the way a LoRaWAN network join is handled. The RAK4200 library Join method blocks until it succeeds of fails, the RAK3172 library Join method returns immediately then an EventHandler is called with the result.

private static void OnJoinCompletionHandler(bool result)
{
	Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Join finished:{result}");

	if (result)
	{
		MessageSendTimer.Change(MessageSendTimerDue, MessageSendTimerPeriod);
	}
}

The new RAK Wireless LoRaWAN modules use the RUI3 AT Commands so the RAK3172 library will most probably be retired and uses as the basis for a generic RUI3 library.

.NET nanoFramework RAK4200 Library Usage

After a two week “soak test” using a Sparkfun Thing Plus ESP32 WROOM and RAK4200 Breakout Board completed with no failures, this final post covers the usage of the RAK4200LoRaWAN-NetNF library in a “real-world” application.

Discovery kit with STM32F769NI MCU and RAK4200 Breakout Board test rig

After a factory reset the DevEUI, JoinEUI (was AppEUI), and AppKey were reset, but the rest of the configuration appeared to be retained.

OK Work Mode: LoRaWAN
Region: AS923
MulticastEnable: false
DutycycleEnable: false
Send_repeat_cnt: 0
Join_mode: OTAA
DevEui: 0000000000000000
AppEui: 0000000000000000
AppKey: 00000000000000000000000000000000
Class: A
Joined Network:false
IsConfirm: unconfirm
AdrEnable: true
EnableRepeaterSupport: false
RX2_CHANNEL_FREQUENCY: 923200000, RX2_CHANNEL_DR:2
RX_WINDOW_DURATION: 3000ms
RECEIVE_DELAY_1: 1000ms
RECEIVE_DELAY_2: 2000ms
JOIN_ACCEPT_DELAY_1: 5000ms
JOIN_ACCEPT_DELAY_2: 6000ms
Current Datarate: 5
Primeval Datarate: 5
ChannelsTxPower: 0
UpLinkCounter: 0
DownLinkCounter: 0

I then ran the RAK4200LoRaWANDeviceClient with DEVICE_DEVEUI_SET (devEui from label on the device), OTAA to configure the AppEui and AppKey and the device connected to The Things Network on the second attempt (typo in the DevEui).

public static void Main()
{
	Result result;

	Debug.WriteLine("devMobile.IoT.RAK4200LoRaWANDeviceClient starting");

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

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

		using (Rak4200LoRaWanDevice device = new Rak4200LoRaWanDevice())
		{
			result = device.Initialise(SerialPortId, 9600, Parity.None, 8, StopBits.One);
			if (result != Result.Success)
			{
				Debug.WriteLine($"Initialise failed {result}");
				return;
			}

#if CONFIRMED
			device.OnMessageConfirmation += OnMessageConfirmationHandler;
#endif
			device.OnReceiveMessage += OnReceiveMessageHandler;

#if FACTORY_RESET
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} FactoryReset");
			result = device.FactoryReset();
			if (result != Result.Success)
			{
				Debug.WriteLine($"FactoryReset failed {result}");
				return;
			}
#endif

#if DEVICE_DEVEUI_SET
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Device EUI");
			result = device.DeviceEui(Config.devEui);
			if (result != Result.Success)
			{
				Debug.WriteLine($"ADR on failed {result}");
				return;
			}
#endif

#if REGION_SET
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Region{Region}");
			result = device.Region(Region);
			if (result != Result.Success)
			{
				Debug.WriteLine($"Region on failed {result}");
				return;
			}
#endif

#if ADR_SET
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} ADR On");
			result = device.AdrOn();
			if (result != Result.Success)
			{
				Debug.WriteLine($"ADR on failed {result}");
				return;
			}
#endif
#if CONFIRMED
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Confirmed");
			result = device.UplinkMessageConfirmationOn();
			if (result != Result.Success)
			{
				Debug.WriteLine($"Confirm on failed {result}");
				return;
			}
#endif
#if UNCONFIRMED
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Unconfirmed");
			result = device.UplinkMessageConfirmationOff();
			if (result != Result.Success)
			{
				Debug.WriteLine($"Confirm off failed {result}");
				return;
			}
#endif

#if OTAA
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} OTAA");
			result = device.OtaaInitialise(Config.JoinEui, Config.AppKey);
			if (result != Result.Success)
			{
				Debug.WriteLine($"OTAA Initialise failed {result}");
				return;
			}
#endif

#if ABP
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} ABP");
			result = device.AbpInitialise(Config.DevAddress, Config.NwksKey, Config.AppsKey);
			if (result != Result.Success)
			{
				Debug.WriteLine($"ABP Initialise failed {result}");
				return;
			}
#endif

			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Join start Timeout:{JoinTimeOut:hh:mm:ss}");
			result = device.Join(JoinTimeOut);
			if (result != Result.Success)
			{
				Debug.WriteLine($"Join failed {result}");
				return;
			}
			Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Join finish");

			while (true)
			{
#if PAYLOAD_BCD
				Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Send Timeout:{SendTimeout:hh:mm:ss} port:{MessagePort} payload BCD:{PayloadBcd}");
				result = device.Send(MessagePort, PayloadBcd, SendTimeout);
#endif
#if PAYLOAD_BYTES
				Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Send Timeout:{SendTimeout:hh:mm:ss} port:{MessagePort} payload Bytes:{BitConverter.ToString(PayloadBytes)}");
				result = device.Send(MessagePort, PayloadBytes, SendTimeout);
#endif
				if (result != Result.Success)
				{
					Debug.WriteLine($"Send failed {result}");
				}

				Thread.Sleep(new TimeSpan(0, 5, 0));
			}
		}
	}
	catch (Exception ex)
	{
		Debug.WriteLine(ex.Message);
	}
}

After configuring my Discovery kit with STM32F769NI MCU and RAK4200 Breakout Board test rig the RAK4200LoRaWANDeviceClient application could successfully connect to The Things Network with just ST_STM32F769I_DISCOVERY, PAYLOAD_BCD or PAYLOAD_BYTES and CONFIRMED or UNCONFIRMED defined.

Visual Studio Debug output for RAK4200LoRaWANDeviceClient minimal configuration connection
The Things Network “Live Data” for RAK4200LoRaWANDeviceClient minimal configuration connection

One of my client’s products has a configuration mode (button pressed as device starts) which enables a serial port (headers on board + FTDI module) for in field configuration of the onboard RAK4200 module.

.NET nanoFramework SX127X LoRa library Regression Testing

After the big refactor the SparkFun LoRa Gateway-1-Channel (ESP32) configuration wouldn’t compile because, the constant for the interrupt pin number didn’t exist (interruptPinNumber vs. dio0PinNumber). A “using” for the Nuget Package nanoFramework.Hardware.Esp32 was also missing.

While “smoke testing” the application I noticed that if I erased the flash, power cycled the device, then ran the application the first execution would fail because the Semtech SX127X could not be detected.

SX127XLoRaDeviceClient first execution startup failure
SX127XLoRaDeviceClient second execution startup success

As part of debugging I built the SparkFun LoRa Gateway-1-Channel (ESP32) version of ShieldSPI and it worked…

After printing the code out and reviewing it I noticed that the Configuration.SetPinFunction for the Serial Peripheral Interface(SPI) Master Out Slave In(MOSI), MOSI(Master In Slave Out) and Clock pins was after the opening of the SPI port.

static void Main(string[] args)
{
	byte 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 dio0PinNumber = 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 dio0PinNumber = 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 dio0PinNumber = PinNumber('J', 1);
#endif
	Console.WriteLine("devMobile.IoT.SX127xLoRaDevice Range Tester starting");

	try
	{
#f 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);
#endif

		var settings = new SpiConnectionSettings(SpiBusId, chipSelectLine)
		{
			ClockFrequency = 1000000,
			Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
			SharingMode = SpiSharingMode.Shared
		};

		using (_gpioController = new GpioController())
		using (SpiDevice spiDevice = new SpiDevice(settings))
		{

#if ESP32_WROOM_32_LORA_1_CHANNEL
			_sx127XDevice = new SX127XDevice(spiDevice, _gpioController, dio0Pin: dio0PinNumber);
#endif

#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
			_sx127XDevice = new SX127XDevice(spiDevice, _gpioController, dio0Pin: dio0PinNumber, resetPin:resetPinNumber);
#endif
...

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

I assume that the first execution after erasing the flash and power cycling the device, the SPI port pin assignments were not configured when the port was opened, then on the next execution the port was pre-configured.

The RangeTester application flashes on onboard Light Emitting Diode(LED) every time a valid message is received. But, on the ESP32 it turned on when the first message arrived and didn’t turn off. After discussion on the nanoFramework Discord this has been identified as an issue(May 2022).

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.

.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.