.NET nanoFramework RAK2305 – UART GPS

The RAKwireless RAK2305 WisBlock WiFi Interface Module module is based on an Expressif ESP32 processor which is supported by the .NET nanoFramework and I wanted try out it out with a RAK1910 GNSS GPS Location Module.

RAK2350, RAK5005-O and RAK1910 with GPS Antenna

The RAK1910 application is based on the TinyGPSPlusNF library by MBoude which parses the NMEA 0183 sentences produced by the RAK1910.

//---------------------------------------------------------------------------------
// Copyright (c) August 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.
//
// RAK Core WisBlock
// https://docs.rakwireless.com/Product-Categories/WisBlock/RAK11200
//
// RAK WisBlock Wireless
// https://docs.rakwireless.com/Product-Categories/WisBlock/RAK2305/Overview/
//
// RAK WisBlock Bases
// https://docs.rakwireless.com/Product-Categories/WisBlock/RAK5005-O

// https://docs.rakwireless.com/Product-Categories/WisBlock/RAK19001
//
// RAK WisBlock Sensor
// https://docs.rakwireless.com/Product-Categories/WisBlock/RAK1910
//
// Uses the library
// https://github.com/mboud/TinyGPSPlusNF
//
// Inspired by
// https://github.com/RAKWireless/WisBlock/tree/master/examples/common/sensors/RAK1910_GPS_UBLOX7
//
// Pins mapped with
// https://docs.rakwireless.com/Knowledge-Hub/Pin-Mapper/
//
// Flash device with
// nanoff --target ESP32_REV0 --serialport COM16 --update
//
//---------------------------------------------------------------------------------
namespace devMobile.IoT.RAK.Wisblock.RAK1910
{
   using System;
   using System.Device.Gpio;
   using System.Diagnostics;
   using System.IO.Ports;
   using System.Threading;

   using nanoFramework.Hardware.Esp32;

   using TinyGPSPlusNF;

   public class Program
   {
      private static TinyGPSPlus _gps;

      public static void Main()
      {
         Debug.WriteLine($"devMobile.IoT.RAK.Wisblock.RAK1910 starting TinyGPS {TinyGPSPlus.LibraryVersion}");

         try
         {
#if RAK11200
            Configuration.SetPinFunction(Gpio.IO21, DeviceFunction.COM2_TX);
            Configuration.SetPinFunction(Gpio.IO19, DeviceFunction.COM2_RX);
#endif
#if RAK2350
            Configuration.SetPinFunction(Gpio.IO21, DeviceFunction.COM2_RX);
            Configuration.SetPinFunction(Gpio.IO19, DeviceFunction.COM2_TX);
#endif

            _gps = new TinyGPSPlus();

            // UART1 with default Max7Q baudrate
            SerialPort serialPort = new SerialPort("COM2", 9600);

            serialPort.DataReceived += SerialDevice_DataReceived;
            serialPort.Open();
            serialPort.WatchChar = '\n';

            // Enable the GPS module GPS 3V3_S/RESET_GPS - IO2 - GPIO27
            GpioController gpioController = new GpioController();

            GpioPin Gps3V3 = gpioController.OpenPin(Gpio.IO27, PinMode.Output);
            Gps3V3.Write(PinValue.High);

            Debug.WriteLine("Waiting...");

            Thread.Sleep(Timeout.Infinite);
         }
         catch (Exception ex)
         {
            Debug.WriteLine($"UBlox MAX7Q initialisation failed {ex.Message}");

            Thread.Sleep(Timeout.Infinite);
         }
      }

      private static void SerialDevice_DataReceived(object sender, SerialDataReceivedEventArgs e)
      {
         // we only care if got EoL character
         if (e.EventType != SerialData.WatchChar)
         {
            return;
         }

         SerialPort serialDevice = (SerialPort)sender;

         string sentence = serialDevice.ReadExisting();

         if (_gps.Encode(sentence))
         {
            if (_gps.Date.IsValid)
            {
               Debug.Write($"{_gps.Date.Year}-{_gps.Date.Month:D2}-{_gps.Date.Day:D2} ");
            }

            if (_gps.Time.IsValid)
            {
               Debug.Write($"{_gps.Time.Hour:D2}:{_gps.Time.Minute:D2}:{_gps.Time.Second:D2}.{_gps.Time.Centisecond:D2} ");
            }

            if (_gps.Location.IsValid)
            {
               Debug.Write($"Lat:{_gps.Location.Latitude.Degrees:F5}° Lon:{_gps.Location.Longitude.Degrees:F5}° ");
            }

            if (_gps.Altitude.IsValid)
            {
               Debug.Write($"Alt:{_gps.Altitude.Meters:F1}M ");
            }

            if (_gps.Location.IsValid)
            {
               Debug.Write($"Hdop:{_gps.Hdop.Value:F2}");
            }

            if (_gps.Date.IsValid || _gps.Time.IsValid || _gps.Location.IsValid || _gps.Altitude.IsValid)
            {
               Debug.WriteLine("");
            }
         }
      }
   }
}

My RAK2305 WisBlock WiFi Interface Module, RAK1910, and RAK5005-O WisBlock Base Board configuration wasn’t supported by the RAK WinBlock Pin Mapper(AUG 2022) tool.

After some experimentation I found that serial port TX/RX lines had to be reversed because both devices would normally be connected to a WisBlock core module.

Visual Studio 2K22 Output Window

.NET nanoFramework RAK2305 – I2C SHT3C

The RAKwireless RAK2305 WisBlock WiFi Interface module is also based on an Expressif ESP32 processor which is supported by the .NET nanoFramework. The RAK2305 WisBlock WiFi Interface module plugs into an IO Slot rather than a Core Slot so I wanted to see if Inter-Integrated Circuit(I2C) bus devices would work with it.

RAL2305 Schematic

The RAK2305 WisBlock WiFi Interface has one I2C port and TXD0/RXD0 are not connected to the base board’s Universal Serial Bus(USB) port.

RAK2305, RAK5005-O and RAK1901 test rig with the FTDI 3V3 pin disconnected

The I2C1 the SDA(serial data) and SCL(serial clock line) have to be mapped to physical pins on the Expressif ESP32 processor using the nanoFramework ESP32 support NuGet. package

                Configuration.SetPinFunction(Gpio.IO04, DeviceFunction.I2C1_DATA);
                Configuration.SetPinFunction(Gpio.IO05, DeviceFunction.I2C1_CLOCK)

The test project uses a RAK1901 WisBlock Temperature and Humidity Sensor(SHTC3) WisBlock Sensor (which has nanoFramework.IoTDevice library support) plugged into a RAK5005 WisBlock Base Board.

//---------------------------------------------------------------------------------
// Copyright (c) September 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.
//
// https://docs.rakwireless.com/Product-Categories/WisBlock/RAK2305
//
// https://docs.rakwireless.com/Product-Categories/WisBlock/RAK11200
//
// https://store.rakwireless.com/products/rak1901-shtc3-temperature-humidity-sensor
//
// https://github.com/nanoframework/nanoFramework.IoT.Device/tree/develop/devices/Shtc3
//
//---------------------------------------------------------------------------------
namespace devMobile.IoT.RAK.Wisblock.RAK1901
{
   using System;
   using System.Diagnostics;
   using System.Device.I2c;
   using System.Threading;

   using nanoFramework.Hardware.Esp32;

   using Iot.Device.Shtc3;

   public class Program
   {
      public static void Main()
      {
         Debug.WriteLine("devMobile.IoT.RAK.Wisblock.RAK11200RAK1901 starting");

         try
         {
            // RAK11200 & RAK2305
            Configuration.SetPinFunction(Gpio.IO04, DeviceFunction.I2C1_DATA);
            Configuration.SetPinFunction(Gpio.IO05, DeviceFunction.I2C1_CLOCK);

            I2cConnectionSettings settings = new(1, Shtc3.DefaultI2cAddress);

            using (I2cDevice device = I2cDevice.Create(settings))
            using (Shtc3 shtc3 = new(device))
            {
               while (true)
               {
                  if (shtc3.TryGetTemperatureAndHumidity(out var temperature, out var relativeHumidity))
                  {
                     Debug.WriteLine($"Temperature {temperature.DegreesCelsius:F1}°C  Humidity {relativeHumidity.Value:F0}%");
                  }

                  Thread.Sleep(10000);
               }
            }
         }
         catch (Exception ex)
         {
            Debug.WriteLine($"SHTC3 initialisation or read failed {ex.Message}");

            Thread.Sleep(Timeout.Infinite);
         }
      }
   }
}
Visual Studio Output window displaying SHT31 temperature & humidity values

I tried to get the RAK2305 WisBlock WiFi Interface going on a RAK19001 WisBlock Dual IO Base Board but the RAK1901 WisBlock Temperature and Humidity Sensor wouldn’t work in any of the six WisBlock sensor ports.

RAK2305, RAK19001 and RAK1903 test rig with the FTDI 3V3 pin disconnected

The header pins I had to soldered onto RAK2305 WisBlock WiFi Interface had to be trimmed to it would fit on the RAK19001 WisBlock Dual IO Base Board.

RAK2305 Clearance issue on RAK19001

One of the RAK19001 WisBlock Dual IO Base Board product features is

“The power supply for the WisBlock modules boards can be controlled by the WisBlock Core modules to minimize power consumption”.

My configuration does not have WisBlock Core module so I think the WisBlock Sensor Module were not powered.

.NET nanoFramework RAK2305

The RAKwireless RAK2305 WisBlock WiFi Interface Module (WisBlock Wireless-IO Slot) is based on an Expressif ESP32 processor which is supported by the .NET nanoFramework. The first step was to solder some headers onto the RAK2305 so I could connect an FTDI module to get Universal Serial Bus(USB) connectivity.

RAK2305 + FTDI Test

After a small delay the RAK2305 appeared in Windows Device Manager on COM4

My first attempt to “flash” the RAK2305 with the nano Firmware Flasher(nanoff) failed

nanoff flashing failure

The RAK2305 Low Level Developer documentation described how to upload software developed with the Arduino tools by putting the ESP32 into “bootloader mode”. This is done by connecting (with the white jumper) the GPIO0 and GND pins on J14, and pressing the reset button.


nanoff flashing success

The first step with any embedded development project is to flash a Light Emitting Diode(LED)….

The RAK2305 has has one onboard LED(TEST_LED) attached to IO18 which I added to the .NET nanoFramework Blinky sample.

//
// Copyright (c) .NET Foundation and Contributors
// See LICENSE file in the project root for full license information.
//
//
using System;
using System.Device.Gpio;
using System.Threading;
using nanoFramework.Hardware.Esp32;

namespace Blinky
{
   public class Program
   {
      private static GpioController s_GpioController;

      public static void Main()
      {
         s_GpioController = new GpioController();

         // pick a board, uncomment one line for GpioPin; default is STM32F769I_DISCO

         // DISCOVERY4: PD15 is LED6 
         //GpioPin led = s_GpioController.OpenPin(PinNumber('D', 15), PinMode.Output);

         // ESP32 DevKit: 4 is a valid GPIO pin in, some boards like Xiuxin ESP32 may require GPIO Pin 2 instead.
         //GpioPin led = s_GpioController.OpenPin(4, PinMode.Output);

         // FEATHER S2: 
         //GpioPin led = s_GpioController.OpenPin(13, PinMode.Output);

         // F429I_DISCO: PG14 is LEDLD4 
         //GpioPin led = s_GpioController.OpenPin(PinNumber('G', 14), PinMode.Output);

         // NETDUINO 3 Wifi: A10 is LED onboard blue
         //GpioPin led = s_GpioController.OpenPin(PinNumber('A', 10), PinMode.Output);

         // QUAIL: PE15 is LED1  
         //GpioPin led = s_GpioController.OpenPin(PinNumber('E', 15), PinMode.Output);

         // STM32F091RC: PA5 is LED_GREEN
         //GpioPin led = s_GpioController.OpenPin(PinNumber('A', 5), PinMode.Output);

         // STM32F746_NUCLEO: PB75 is LED2
         //GpioPin led = s_GpioController.OpenPin(PinNumber('B', 7), PinMode.Output);

         //STM32F769I_DISCO: PJ5 is LD2
         //GpioPin led = s_GpioController.OpenPin(PinNumber('J', 5), PinMode.Output);

         // ST_B_L475E_IOT01A: PB14 is LD2
         //GpioPin led = s_GpioController.OpenPin(PinNumber('B', 14), PinMode.Output);

         // STM32L072Z_LRWAN1: PA5 is LD2
         //GpioPin led = s_GpioController.OpenPin(PinNumber('A', 5), PinMode.Output);

         // TI CC13x2 Launchpad: DIO_07 it's the green LED
         //GpioPin led = s_GpioController.OpenPin(7, PinMode.Output);

         // TI CC13x2 Launchpad: DIO_06 it's the red LED  
         //GpioPin led = s_GpioController.OpenPin(6, PinMode.Output);

         // ULX3S FPGA board: for the red D22 LED from the ESP32-WROOM32, GPIO5
         //GpioPin led = s_GpioController.OpenPin(5, PinMode.Output);

         // Silabs SLSTK3701A: LED1 PH14 is LLED1
         //GpioPin led = s_GpioController.OpenPin(PinNumber('H', 14), PinMode.Output);

         // RAK11200 on RAK5005
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO12, PinMode.Output); // LED1 Green
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO02, PinMode.Output); // LED2 Blue

         // RAK11200 on RAK19001 needs battery connected or power switch in rechargeable position.
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO12, PinMode.Output); // LED1 Green
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO02, PinMode.Output); // LED2 Blue

         // RAK2305 
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO18, PinMode.Output); // LED Green (Test LED) on device

         // RAK2305 On 5005 throws exceptions
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO34, PinMode.Output); // LED1 Green
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO35, PinMode.Output); // LED2 Blue

         // RAK2305 On 17001 throws exceptions
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO34, PinMode.Output); // LED1 Green
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO35, PinMode.Output); // LED2 Blue

         led.Write(PinValue.Low);

         while (true)
         {
            led.Toggle();
            Thread.Sleep(125);
            led.Toggle();
            Thread.Sleep(125);
            led.Toggle();
            Thread.Sleep(125);
            led.Toggle();
            Thread.Sleep(525);
         }
      }

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

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

I added the RAK2305 configuration to my version of the nanoFramework Blinky sample and could reliably flash the onboard LED.

.NET nanoFramework RAK11200 – UART GPS

The RAKwireless RAK11200 WisBlock WiFi Module module is based on an Expressif ESP32 processor which is supported by the .NET nanoFramework and I wanted try out it out with a RAK1910 GNSS GPS Location Module.

RAK11200, RAK5005-O and RAK1910 with GPS Antenna

The RAK WinBlock Pin Mapper tool output for RAK1910, RAK5005-O WisBlock Base Board and RAK11200

The test application is based on the TinyGPSPlusNF library by MBoude which parses the NMEA 0183 sentences produced by the RAK1910.

public class Program
{
    private static TinyGPSPlus _gps;

    public static void Main()
    {
        Debug.WriteLine($"devMobile.IoT.RAK.Wisblock.Max7Q starting TinyGPS {TinyGPSPlus.LibraryVersion}");

        Configuration.SetPinFunction(Gpio.IO21, DeviceFunction.COM2_TX);
        Configuration.SetPinFunction(Gpio.IO19, DeviceFunction.COM2_RX);

        _gps = new TinyGPSPlus();

        // UART1 with default Max7Q baudrate
        SerialPort serialPort = new SerialPort("COM2", 9600);

        serialPort.DataReceived += SerialDevice_DataReceived;
        serialPort.Open();
        serialPort.WatchChar = '\n';

         // // Enable the with GPS 3V3_S/RESET_GPS - IO2 - GPIO27
        GpioController gpioController = new GpioController();

        GpioPin Gps3V3 = gpioController.OpenPin(Gpio.IO27, PinMode.Output);
        Gps3V3.Write(PinValue.High);

        Debug.WriteLine("Waiting...");

        Thread.Sleep(Timeout.Infinite);
    }

    private static void SerialDevice_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        // we only care if got EoL character
        if (e.EventType != SerialData.WatchChar)
        {
            return;
        }

        SerialPort serialDevice = (SerialPort)sender;

        string sentence = serialDevice.ReadExisting();

        if (_gps.Encode(sentence))
        {
            if (_gps.Date.IsValid)
            {
                Debug.Write($"{_gps.Date.Year}-{_gps.Date.Month:D2}-{_gps.Date.Day:D2} ");
            }

            if (_gps.Time.IsValid)
            {
                Debug.Write($"{_gps.Time.Hour:D2}:{_gps.Time.Minute:D2}:{_gps.Time.Second:D2}.{_gps.Time.Centisecond:D2} ");
            }

            if (_gps.Location.IsValid)
            {
                Debug.Write($"Lat:{_gps.Location.Latitude.Degrees:F5}° Lon:{_gps.Location.Longitude.Degrees:F5}° ");
            }

            if (_gps.Altitude.IsValid)
            {
                Debug.Write($"Alt:{_gps.Altitude.Meters:F1}M");
            }

            if (_gps.Date.IsValid || _gps.Time.IsValid || _gps.Location.IsValid || _gps.Altitude.IsValid)
            {
                Debug.WriteLine("");
            }
        }
    }
}
Visual Studio 2K22 Output Window

.NET nanoFramework RAK11200 – I2C SHT3C & SHT31

The RAKwireless RAK11200 WisBlock WiFi Module module is based on an Expressif ESP32 processor which is supported by the .NET nanoFramework and I wanted to explore the different ways Inter-Integrated Circuit(I2C) devices could be connected.

The RAK11200 WisBlock WiFi Module has two I2C ports and on the RAK5005 WisBlock Base Board the Wisblock Sensor, and RAK1920 WisBlock Sensor Adapter Module Grove Socket are connected to I2C1.

RAK11200 Schematic

The I2C1 the SDA(serial data) and SCL(serial clock line) have to be mapped to physical pins on the RAK11200 WisBlock WiFi Module using the nanoFramework ESP32 support NuGet. package

                Configuration.SetPinFunction(Gpio.IO04, DeviceFunction.I2C1_DATA);
                Configuration.SetPinFunction(Gpio.IO05, DeviceFunction.I2C1_CLOCK)

The first sample project uses a RAK1901 SHTC3 WisBlock Sensor because it plugs into the RAK5005 WisBlock Base Board.

RAK5005 Baseboard, RAK1901 Sensor and RAK11200 Core WisBlock modules
public static void Main()
{
    Debug.WriteLine("devMobile.IoT.RAK.Wisblock.SHTC3 starting");

    try
    {
        Configuration.SetPinFunction(Gpio.IO04, DeviceFunction.I2C1_DATA);
        Configuration.SetPinFunction(Gpio.IO05, DeviceFunction.I2C1_CLOCK);

        I2cConnectionSettings settings = new(1, Shtc3.DefaultI2cAddress);

        using (I2cDevice device = I2cDevice.Create(settings))
        using (Shtc3 shtc3 = new(device))
        {
            while (true)
            {
                if (shtc3.TryGetTemperatureAndHumidity(out var temperature, out var relativeHumidity))
                {
                    Debug.WriteLine($"Temperature {temperature.DegreesCelsius:F1}°C  Humidity {relativeHumidity.Value:F0}%");
                }

                Thread.Sleep(10000);
            }
        }
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"SHTC3 initialisation or read failed {ex.Message}");

        Thread.Sleep(Timeout.Infinite);
    }
}
Visual Studio Output window displaying SHT3C temperature & humidity values

The second sample uses a Seeedstudio Grove – Temperature & Humidity Sensor (SHT31) pluged into a RAK1920 Sensor Adapter for Click, QWIIC and Grove Modules.

RAK5005 Baseboard, RAK1920 Sensor, RAK11200 Core WisBlock modules and Seeedstudio Grove SHT31
public static void Main()
{
    Debug.WriteLine("devMobile.IoT.RAK.Wisblock.SHT31 starting");

    try
    {
        Configuration.SetPinFunction(Gpio.IO04, DeviceFunction.I2C1_DATA);
        Configuration.SetPinFunction(Gpio.IO05, DeviceFunction.I2C1_CLOCK);

        I2cConnectionSettings settings = new(1, (byte)I2cAddress.AddrLow);

        using (I2cDevice device = I2cDevice.Create(settings))
        using (Sht3x sht31 = new(device))
        {

            while (true)
            {
                var temperature = sht31.Temperature;
                var relativeHumidity = sht31.Humidity;

                Debug.WriteLine($"Temperature {temperature.DegreesCelsius:F1}°C  Humidity {relativeHumidity.Value:F0}%");

                Thread.Sleep(10000);
            }
        }
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"SHT31 initialisation or read failed {ex.Message}");

        Thread.Sleep(Timeout.Infinite);
     }
}
Visual Studio Output window displaying SHT31 temperature & humidity values

The SHTC3 and SHT31 sensors were used because they both have nanoFramework.IoTDevice library support.

.NET nanoFramework RAK11200

The RAKwireless RAK11200 WisBlock WiFi Module module is based on an Expressif ESP32 processor which is supported by the .NET nanoFramework. The first step was to mount the RAK11200 on a RAK5005 WisBlock Base Board to get Universal Serial Bus(USB) connectivity.

RAK11200 Mounted on RAK5005 base board

My first attempt “flash” the RAK11200 with the nano Firmware Flasher(nanoff) failed badly

nanoff flashing failure

The RAK11200 documentation described how to upload software developed with the Arduino tools by putting the ESP32 into “bootloader mode” by connecting the BOOT0 and GND pins, then pressing the reset button.

RAK11200 BOOT0 & GND pins connected to

After some “trial and error” the download process worked pretty reliably…

nanoff flashing success

The first step with any embedded development project is to flash a Light Emitting Diode(LED)….

RAK11200 Schematic

The RAK11200 has two LEDs, a blue attached to IO02 and a green one attached to IO12.

//
// Copyright (c) .NET Foundation and Contributors
// See LICENSE file in the project root for full license information.
//
//
using System;
using System.Device.Gpio;
using System.Threading;
using nanoFramework.Hardware.Esp32;

namespace Blinky
{
   public class Program
   {
      private static GpioController s_GpioController;
      public static void Main()
      {
         s_GpioController = new GpioController();

         // pick a board, uncomment one line for GpioPin; default is STM32F769I_DISCO

         // DISCOVERY4: PD15 is LED6 
         //GpioPin led = s_GpioController.OpenPin(PinNumber('D', 15), PinMode.Output);

         // ESP32 DevKit: 4 is a valid GPIO pin in, some boards like Xiuxin ESP32 may require GPIO Pin 2 instead.
         //GpioPin led = s_GpioController.OpenPin(4, PinMode.Output);

         // FEATHER S2: 
         //GpioPin led = s_GpioController.OpenPin(13, PinMode.Output);

         // F429I_DISCO: PG14 is LEDLD4 
         //GpioPin led = s_GpioController.OpenPin(PinNumber('G', 14), PinMode.Output);

         // NETDUINO 3 Wifi: A10 is LED onboard blue
         //GpioPin led = s_GpioController.OpenPin(PinNumber('A', 10), PinMode.Output);

         // QUAIL: PE15 is LED1  
         //GpioPin led = s_GpioController.OpenPin(PinNumber('E', 15), PinMode.Output);

         // STM32F091RC: PA5 is LED_GREEN
         //GpioPin led = s_GpioController.OpenPin(PinNumber('A', 5), PinMode.Output);

         // STM32F746_NUCLEO: PB75 is LED2
         //GpioPin led = s_GpioController.OpenPin(PinNumber('B', 7), PinMode.Output);

         //STM32F769I_DISCO: PJ5 is LD2
         //GpioPin led = s_GpioController.OpenPin(PinNumber('J', 5), PinMode.Output);

         // ST_B_L475E_IOT01A: PB14 is LD2
         //GpioPin led = s_GpioController.OpenPin(PinNumber('B', 14), PinMode.Output);

         // STM32L072Z_LRWAN1: PA5 is LD2
         //GpioPin led = s_GpioController.OpenPin(PinNumber('A', 5), PinMode.Output);

         // TI CC13x2 Launchpad: DIO_07 it's the green LED
         //GpioPin led = s_GpioController.OpenPin(7, PinMode.Output);

         // TI CC13x2 Launchpad: DIO_06 it's the red LED  
         //GpioPin led = s_GpioController.OpenPin(6, PinMode.Output);

         // ULX3S FPGA board: for the red D22 LED from the ESP32-WROOM32, GPIO5
         //GpioPin led = s_GpioController.OpenPin(5, PinMode.Output);

         // Silabs SLSTK3701A: LED1 PH14 is LLED1
         //GpioPin led = s_GpioController.OpenPin(PinNumber('H', 14), PinMode.Output);

         // RAK11200 on RAK5005
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO12, PinMode.Output); // LED1 Green
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO02, PinMode.Output); // LED2 Blue

         // RAK11200 on RAK19001 needs battery connected or power switch in rechargeable position.
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO12, PinMode.Output); // LED1 Green
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO02, PinMode.Output); // LED2 Blue

         // RAK2305 
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO18, PinMode.Output); // LED Green (Test LED) on device

         // RAK2305 On 5005 throws exceptions
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO34, PinMode.Output); // LED1 Green
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO35, PinMode.Output); // LED2 Blue

         // RAK2305 On 17001 throws exceptions
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO34, PinMode.Output); // LED1 Green
         //GpioPin led = s_GpioController.OpenPin(Gpio.IO35, PinMode.Output); // LED2 Blue

         led.Write(PinValue.Low);

         while (true)
         {
            led.Toggle();
            Thread.Sleep(125);
            led.Toggle();
            Thread.Sleep(125);
            led.Toggle();
            Thread.Sleep(125);
            led.Toggle();
            Thread.Sleep(525);
         }
      }

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

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

I added the RAK11200 configuration to my version of the nanoFramework Blinky sample and could reliably flash either of the LEDs.

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