.NET nanoFramework RAK4200 LoRaWAN library ABP Join

After getting my RAKwireless RAK4200 module to reliably connect to The Things Network(TTN) using Over The Air Activation(OTAA) the next step was to built an Activation By Personalisation(ABP) sample application.

I modified the NetworkJoinOTAA sample(based on the asynchronous version of BreakOutSerial) to send the required 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.Text;
   using System.Threading;
#if ESP32_WROOM
   using global::nanoFramework.Hardware.Esp32; ///need NuGet nanoFramework.Hardware.Esp32
#endif

   public class Program
   {
#if ST_STM32F769I_DISCOVERY
      private const string SerialPortId = "COM6";
#endif
#if ESP32_WROOM
      private const string SerialPortId = "COM2";
#endif
      private const string DevEui = "...";
      private const string AppEui = "...";
      private const string AppKey = "...";
      private const byte MessagePort = 1;
      private const string Payload = "01020304"; // Is AQIDBA==

      public static void Main()
      {
         string response;

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

         try
         {
#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 (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.ReadBufferSize = 128; 
               //serialDevice.ReadBufferSize = 256; 
               serialDevice.ReadBufferSize = 512;
               //serialDevice.ReadBufferSize = 1024;

               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
               Debug.WriteLine("lora:work_mode:0");
               serialDevice.WriteLine("at+set_config=lora:work_mode:0");
               Thread.Sleep(1500);

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

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

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

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

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

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

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

               Debug.WriteLine("lora:adr");
               serialDevice.WriteLine("at+set_config=lora:adr:1");
               Thread.Sleep(500);

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

               byte counter = 1;
               while (true)
               {
                  // Send the BCD messages
                  string payload = $"{Payload}{counter:X2}";
                  Debug.WriteLine($"at+send=lora:{MessagePort}:{payload}");
                  serialDevice.WriteLine($"at+send=lora:{MessagePort}:{payload}");

                  counter += 1;

                  Thread.Sleep(300000);
               }
            }
         }
         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 NetworkJoinABP application assumes that all of the AT commands succeed.

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

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

.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 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 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 DIO0,DIO1,DIO2,DIO3,DIO4,DIO5

All the previous versions of my .NET nanoFramework Semtech SX127X (LoRa® Mode) library only supported a Dio0 (RegDioMapping1 bits 6&7) EventHandler. This version supports mapping Dio0, Dio1, Dio2, Dio3, Dio4 and Dio5.

DIO Mapping in LoRa Mode
RegDioMapping1 & RegDioMapping2 options

The Dragino Arduino Shield featuring LoRa® technology does not have Dio3 and Dio4 connected so I have been unable to test that functionality.

Dragino LoRa Shield Pin Mapping

The SX127XLoRaDeviceClient main now has OnRxTimeout, OnReceive, OnPayloadCrcError, OnValidHeader, OnTransmit, OnChannelActivityDetectionDone, OnFhssChangeChannel, and OnChannelActivityDetected event handlers (Based on RegIrqFlags bit ordering)

static void Main(string[] args)
{
	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 dio0PinNumber = PinNumber('A', 3);
	// Arduino D6 - PB9
	int dio1PinNumber = PinNumber('B', 9);
	// Arduino D7
	int dio2PinNumber = PinNumber('A', 1);
	// Not connected on Dragino LoRa shield
	//int dio3PinNumber = PinNumber('A', 1);
	//  Not connected on Dragino LoRa shield
	//int dio4PinNumber = PinNumber('A', 1);
	// Arduino D8
	int dio5PinNumber = PinNumber('A', 0);
#endif
...
	Console.WriteLine("devMobile.IoT.SX127xLoRaDevice Client starting");

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

		using (SpiDevice spiDevice = new SpiDevice(settings))
		using (GpioController gpioController = new GpioController())
		{
...
#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
			sx127XDevice = new SX127XDevice(spiDevice, gpioController, dio0Pin:dio0PinNumber, resetPin:resetPinNumber, dio1Pin: dio1PinNumber, dio2Pin: dio2PinNumber);
#endif

			sx127XDevice.Initialise(Frequency
						, lnaGain: Configuration.RegLnaLnaGain.Default
						, lnaBoost: true
						, powerAmplifier: Configuration.RegPAConfigPASelect.PABoost							
						, rxPayloadCrcOn: true
						, rxDoneignoreIfCrcMissing: false
						);

#if DEBUG
			sx127XDevice.RegisterDump();
#endif

			//sx127XDevice.OnRxTimeout += Sx127XDevice_OnRxTimeout;
			sx127XDevice.OnReceive += SX127XDevice_OnReceive;
			//sx127XDevice.OnPayloadCrcError += Sx127XDevice_OnPayloadCrcError;
			//sx127XDevice.OnValidHeader += Sx127XDevice_OnValidHeader;
			sx127XDevice.OnTransmit += SX127XDevice_OnTransmit;
			//sx127XDevice.OnChannelActivityDetectionDone += Sx127XDevice_OnChannelActivityDetectionDone;
			//sx127XDevice.OnFhssChangeChannel += Sx127XDevice_OnFhssChangeChannel;
			//sx127XDevice.OnChannelActivityDetected += SX127XDevice_OnChannelActivityDetected;

			sx127XDevice.Receive();
			//sx127XDevice.ChannelActivityDetect();

			Thread.Sleep(500);

			while (true)
			{
				string messageText = $"Hello LoRa from .NET nanoFramework Count {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);

				Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss} Random {sx127XDevice.Random()}");
			}
		}
	}
	catch (Exception ex)
	{
		Console.WriteLine(ex.Message);
	}
}

The Dio0 pin number is the only required pin number parameter, the resetPin, and Dio1 thru Dio5 pin numbers are optional. All the RegDioMapping1 and RegDioMapping2 mappings are disabled on intialisation so there should be no events while the SX127X is being configured.

public SX127XDevice(SpiDevice spiDevice, GpioController gpioController,
	int dio0Pin,
	int resetPin = 0, // Odd order so as not to break exisiting code
	int dio1Pin = 0,
	int dio2Pin = 0,
	int dio3Pin = 0,
	int dio4Pin = 0,
	int dio5Pin = 0
	)
{
	_gpioController = gpioController;

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

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

	_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");
	}

	// See Table 18 DIO Mapping LoRa® Mode
	Configuration.RegDioMapping1 regDioMapping1Value = Configuration.RegDioMapping1.Dio0None;
	regDioMapping1Value |= Configuration.RegDioMapping1.Dio1None;
	regDioMapping1Value |= Configuration.RegDioMapping1.Dio2None;
	regDioMapping1Value |= Configuration.RegDioMapping1.Dio3None;
	_registerManager.WriteByte((byte)Configuration.Registers.RegDioMapping1, (byte)regDioMapping1Value);

	// Currently no easy way to test this with available hardware
	//Configuration.RegDioMapping2 regDioMapping2Value = Configuration.RegDioMapping2.Dio4None;
	//regDioMapping2Value = Configuration.RegDioMapping2.Dio5None;
	//_registerManager.WriteByte((byte)Configuration.Registers.RegDioMapping2, (byte)regDioMapping2Value);

	// Interrupt pin for RXDone, TXDone, and CadDone notification 
	_gpioController.OpenPin(dio0Pin, PinMode.InputPullDown);
	_gpioController.RegisterCallbackForPinValueChangedEvent(dio0Pin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);

	// RxTimeout, FhssChangeChannel, and CadDetected
	if (dio1Pin != 0)
	{
		_gpioController.OpenPin(dio1Pin, PinMode.InputPullDown);
		_gpioController.RegisterCallbackForPinValueChangedEvent(dio1Pin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
	}

	// FhssChangeChannel, FhssChangeChannel, and FhssChangeChannel
	if (dio2Pin != 0)
	{
		_gpioController.OpenPin(dio2Pin, PinMode.InputPullDown);
		_gpioController.RegisterCallbackForPinValueChangedEvent(dio2Pin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
	}

	// CadDone, ValidHeader, and PayloadCrcError
	if (dio3Pin != 0)
	{
		_gpioController.OpenPin(dio3Pin, PinMode.InputPullDown);
		_gpioController.RegisterCallbackForPinValueChangedEvent(dio3Pin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
	}

	// CadDetected, PllLock and PllLock
	if (dio4Pin != 0)
	{
		_gpioController.OpenPin(dio4Pin, PinMode.InputPullDown);
		_gpioController.RegisterCallbackForPinValueChangedEvent(dio4Pin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
	}

	// ModeReady, ClkOut and ClkOut
	if (dio5Pin != 0)
	{
		_gpioController.OpenPin(dio5Pin, PinMode.InputPullDown);
		_gpioController.RegisterCallbackForPinValueChangedEvent(dio5Pin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
	}
}

The same event handler (InterruptGpioPin_ValueChanged) is used for Dio0 thru Dio5. Each event has a “process” method and the RegIrqFlags register controls which one(s) are called.

private void InterruptGpioPin_ValueChanged(object sender, PinValueChangedEventArgs pinValueChangedEventArgs)
{
	Byte regIrqFlagsToClear = (byte)Configuration.RegIrqFlags.ClearNone;

	// Read RegIrqFlags to see what caused the interrupt
	Byte irqFlags = _registerManager.ReadByte((byte)Configuration.Registers.RegIrqFlags);

	//Console.WriteLine($"IrqFlags 0x{irqFlags:x} Pin:{pinValueChangedEventArgs.PinNumber}");

	// Check RxTimeout for inbound message
	if ((irqFlags & (byte)Configuration.RegIrqFlagsMask.RxTimeoutMask) == (byte)Configuration.RegIrqFlags.RxTimeout)
	{
		regIrqFlagsToClear |= (byte)Configuration.RegIrqFlags.RxTimeout;

		ProcessRxTimeout(irqFlags);
	}

	// Check RxDone for inbound message
	if ((irqFlags & (byte)Configuration.RegIrqFlagsMask.RxDoneMask) == (byte)Configuration.RegIrqFlags.RxDone)
	{
		regIrqFlagsToClear |= (byte)Configuration.RegIrqFlags.RxDone;

		ProcessRxDone(irqFlags);
	}

	// Check PayLoadCrcError for inbound message
	if ((irqFlags & (byte)Configuration.RegIrqFlagsMask.PayLoadCrcErrorMask) == (byte)Configuration.RegIrqFlags.PayLoadCrcError)
	{
		regIrqFlagsToClear |= (byte)Configuration.RegIrqFlags.PayLoadCrcError;

		ProcessPayloadCrcError(irqFlags);
	}

	// Check ValidHeader for inbound message
	if ((irqFlags & (byte)Configuration.RegIrqFlagsMask.ValidHeaderMask) == (byte)Configuration.RegIrqFlags.ValidHeader)
	{
		regIrqFlagsToClear |= (byte)Configuration.RegIrqFlags.ValidHeader;

		ProcessValidHeader(irqFlags);
	}

	// Check TxDone for outbound message
	if ((irqFlags & (byte)Configuration.RegIrqFlagsMask.TxDoneMask) == (byte)Configuration.RegIrqFlags.TxDone)
	{
		regIrqFlagsToClear |= (byte)Configuration.RegIrqFlags.TxDone;

		ProcessTxDone(irqFlags);
	}

	// Check Channel Activity Detection done 
	if (((irqFlags & (byte)Configuration.RegIrqFlagsMask.CadDoneMask) == (byte)Configuration.RegIrqFlags.CadDone))
	{
		regIrqFlagsToClear |= (byte)Configuration.RegIrqFlags.CadDone;

		ProcessChannelActivityDetectionDone(irqFlags);
	}

	// Check FhssChangeChannel for inbound message
	if ((irqFlags & (byte)Configuration.RegIrqFlagsMask.FhssChangeChannelMask) == (byte)Configuration.RegIrqFlags.FhssChangeChannel)
	{
		regIrqFlagsToClear |= (byte)Configuration.RegIrqFlags.FhssChangeChannel;

		ProcessFhssChangeChannel(irqFlags);
	}

	// Check Channel Activity Detected 
	if (((irqFlags & (byte)Configuration.RegIrqFlagsMask.CadDetectedMask) == (byte)Configuration.RegIrqFlags.CadDetected))
	{
		regIrqFlagsToClear |= (byte)Configuration.RegIrqFlags.CadDetected;

		ProcessChannelActivityDetected(irqFlags);
	}

	_registerManager.WriteByte((byte)Configuration.Registers.RegIrqFlags, regIrqFlagsToClear);
}

private void ProcessRxTimeout(byte irqFlags)
{
	OnRxTimeoutEventArgs onRxTimeoutArgs = new OnRxTimeoutEventArgs();

	OnRxTimeout?.Invoke(this, onRxTimeoutArgs);
}

private void ProcessRxDone(byte irqFlags)
{
	byte[] payloadBytes;
...
}

The RegIrqFlags bits are cleared individually (with regIrqFlagsToClear) at the end of the event handler. Initially I cleared all the flags by writing 0xFF to RegIrqFlags but this caused issues when there were multiple bits set e.g. CadDone along with CadDetected.

devMobile.IoT.SX127xLoRaDevice Client starting
Register dump
Register 0x01 - Value 0X80
...
Register 0x4d - Value 0X84

00:00:09-CAD Detection Done
00:00:09-CAD Detected
00:00:09-RX PacketSnr 0.0 Packet RSSI -100dBm RSSI -96dBm = 9 byte message hello 41
00:00:09-RX PacketSnr 0.0 Packet RSSI -100dBm RSSI -96dBm = 9 byte message hello 42
00:00:09-RX PacketSnr 0.0 Packet RSSI -100dBm RSSI -96dBm = 9 byte message hello 43
00:00:09-RX PacketSnr 0.0 Packet RSSI -100dBm RSSI -96dBm = 9 byte message hello 44
00:00:09-RX PacketSnr 0.0 Packet RSSI -100dBm RSSI -96dBm = 9 byte message hello 45
00:00:09-RX PacketSnr 0.0 Packet RSSI -100dBm RSSI -94dBm = 9 byte message hello 46
00:00:09-RX PacketSnr 0.0 Packet RSSI -99dBm RSSI -94dBm = 9 byte message hello 47
00:00:19-RX PacketSnr 0.0 Packet RSSI -100dBm RSSI -96dBm = 9 byte message hello 48

It took some experimentation with the SX127xLoRaDeviceClient application to “reliably” trigger events for testing. To generate CAD Detected event, I had to modify one of the Arduino-LoRa sample applications to send messages without a delay, then have it running as the SX127xLoRaDeviceClient application was starting.

.NET nanoFramework SX127X LoRa library RegPaConfig RegPaDac

While updating my .NET nanoFramework Semtech SX127X library I revisited (because I thought it might still be wrong) how the output power is calculated. I started with the overview of the transmitter architecture in in the datasheet…

SX127X Overview of transmission pipeline

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. (The AdaFruit version of the RadioHead library has differences to the Semtech Lora-net/LoRaMac-Node libraries)

RegPaConfig & RegOcp 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.

High power mode overview
RegPaDac register configuration options

The RegOcp (over current protection) has to be relaxed for the higher power modes

RegPaConfig register configuration options

I started with the Semtech Lora-net/LoRaMac-Node library which reads the RegPaConfig, RegPaSelect and RegPaDac registers then does any updates required.

void SX1276SetRfTxPower( int8_t power )
{
    uint8_t paConfig = 0;
    uint8_t paDac = 0;

    paConfig = SX1276Read( REG_PACONFIG );
    paDac = SX1276Read( REG_PADAC );

    paConfig = ( paConfig & RF_PACONFIG_PASELECT_MASK ) | SX1276GetPaSelect( power );

    if( ( paConfig & RF_PACONFIG_PASELECT_PABOOST ) == RF_PACONFIG_PASELECT_PABOOST )
    {
        if( power > 17 )
        {
            paDac = ( paDac & RF_PADAC_20DBM_MASK ) | RF_PADAC_20DBM_ON;
        }
        else
        {
            paDac = ( paDac & RF_PADAC_20DBM_MASK ) | RF_PADAC_20DBM_OFF;
        }
        if( ( paDac & RF_PADAC_20DBM_ON ) == RF_PADAC_20DBM_ON )
        {
            if( power < 5 )
            {
                power = 5;
            }
            if( power > 20 )
            {
                power = 20;
            }
            paConfig = ( paConfig & RF_PACONFIG_OUTPUTPOWER_MASK ) | ( uint8_t )( ( uint16_t )( power - 5 ) & 0x0F );
        }
        else
        {
            if( power < 2 )
            {
                power = 2;
            }
            if( power > 17 )
            {
                power = 17;
            }
            paConfig = ( paConfig & RF_PACONFIG_OUTPUTPOWER_MASK ) | ( uint8_t )( ( uint16_t )( power - 2 ) & 0x0F );
        }
    }
    else
    {
        if( power > 0 )
        {
            if( power > 15 )
            {
                power = 15;
            }
            paConfig = ( paConfig & RF_PACONFIG_MAX_POWER_MASK & RF_PACONFIG_OUTPUTPOWER_MASK ) | ( 7 << 4 ) | ( power );
        }
        else
        {
            if( power < -4 )
            {
                power = -4;
            }
            paConfig = ( paConfig & RF_PACONFIG_MAX_POWER_MASK & RF_PACONFIG_OUTPUTPOWER_MASK ) | ( 0 << 4 ) | ( power + 4 );
        }
    }
    SX1276Write( REG_PACONFIG, paConfig );
    SX1276Write( REG_PADAC, paDac );
}

I also reviewed the Arduino-LoRa Semtech library which only writes to the RegPaConfig, RegPaSelect and RegPaDac registers.

void LoRaClass::setTxPower(int level, int outputPin)
{
  if (PA_OUTPUT_RFO_PIN == outputPin) {
    // RFO
    if (level < 0) {
      level = 0;
    } else if (level > 14) {
      level = 14;
    }

    writeRegister(REG_PA_CONFIG, 0x70 | level);
  } else {
    // PA BOOST
    if (level > 17) {
      if (level > 20) {
        level = 20;
      }

      // subtract 3 from level, so 18 - 20 maps to 15 - 17
      level -= 3;

      // High Power +20 dBm Operation (Semtech SX1276/77/78/79 5.4.3.)
      writeRegister(REG_PA_DAC, 0x87);
      setOCP(140);
    } else {
      if (level < 2) {
        level = 2;
      }
      //Default value PA_HF/LF or +17dBm
      writeRegister(REG_PA_DAC, 0x84);
      setOCP(100);
    }

    writeRegister(REG_PA_CONFIG, PA_BOOST | (level - 2));
  }
}

I updated the output power configuration code in the Initialise method of the SX127X library. After reviewing the SX127X datasheet I extended the way the pOut is calculated in RFO mode. The code uses two values for MaxPower(RegPAConfigMaxPower.Min & RegPAConfigMaxPower.Max) so that the full RTO output power range was available.

// 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);
	}
}

The formula for pOut and pMax in RegPaConfig documentation is included in the source code so I could manually calculate (including edge cases) the values as part of my testing. I ran the SX127XLoRaDeviceClient and inspected the PaConfig & RegPaDac in the Visual Studio 2022 debugger.

PABoost
Output power = 1
Output power = 21
Exception

Output power = 2
PaConfig = 192
RegPaDac = normal
	1100 0000

Output power = 16
PaConfig = 206
RegPaDac = normal
	1100 1110

Output power = 17
PaConfig = 204
RegPacDac = Normal
	1100 1100

Output power = 18
PaConfig = 205
RegPacDac = Boost
	1100 1101

Output power = 19
PaConfig = 206
RegPacDac = Boost
	1100 1110

Output power = 20
PaConfig = 207
RegPacDac = Boost
	1100 1111

RFO
Output power = -5
Output power = 16
Exception

Output power = -4
PAConfig = 0
	0000 0000

Output power = -1
PAConfig = 3
	0000 0011

Output power = 0
PAConfig = 4
	0000 0100

Output power = 1
PAConfig = 113
	0111 0001

OutputPower = 14
PAConfig = 126
	0111 1110

OutputPower = 15
PAConfig = 127
	0111 1111

I need to borrow some test gear to check my implementation

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