RFM9X.NetNF on Netduino

After publishing my RFM9X.NetMF library working I noticed a Netduino 3 Wifi sitting at the back of my desk. I have got a few Netduinos and NanoFramework support (as a reference platform) for the Netduino 3 Wifi had caught my attention.

Netduino 3 Wifi LoRa test rig

The first step was to get the chip select, reset and Serial Peripheral interface(SPI) configurations sorted. I’m using a Dragino LoRa shield for Arduino and a Netduino 3 Wifi.

Dragino LoRa Shield Schematic

The first step was to figure out the configuration using the 00.Shield project. After some experimentation I figured out the SPI port connected to D10-D13 was SPI2 (SPI1 is connected to the MicroSD port)

//---------------------------------------------------------------------------------
// Copyright (c) April 2020, 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 ESP32_WROOM_32_LORA_1_CHANNEL   //nanoff --target ESP32_WROOM_32 --serialport COM4 --update
#define NETDUINO3_WIFI   // nanoff --target NETDUINO3_WIFI --update
//NOTE May 2020 ST_NUCLEO64_F091RC device doesn't work something broken in SPI configuration
//#define ST_NUCLEO64_F091RC // nanoff --target ST_NUCLEO64_F091RC --update
//#define ST_STM32F429I_DISCOVERY       //nanoff --target ST_STM32F429I_DISCOVERY --update
//NOTE May 2020 ST_STM32F769I_DISCOVERY device doesn't work SPI2 mappings broken 
//#define ST_STM32F769I_DISCOVERY      // nanoff --target ST_STM32F769I_DISCOVERY --update 
namespace devMobile.IoT.Rfm9x.ShieldSPI
{
   using System;
   using System.Diagnostics;
   using System.Threading;

   using Windows.Devices.Gpio;
   using Windows.Devices.Spi;

#if ESP32_WROOM_32_LORA_1_CHANNEL
   using nanoFramework.Hardware.Esp32;
#endif

   public class Program
   {
      private const byte RegVersion = 0x42;
#if ESP32_WROOM_32_LORA_1_CHANNEL
      private const string SpiBusId = "SPI1";
#endif
#if NETDUINO3_WIFI
      private const string SpiBusId = "SPI2";
#endif
#if ST_NUCLEO64_F091RC
      private const string SpiBusId = "SPI1";
#endif
#if ST_STM32F429I_DISCOVERY
      private const string SpiBusId = "SPI5";
#endif
#if ST_STM32F769I_DISCOVERY
      private const string SpiBusId = "SPI5";
#endif

      public static void Main()
      {
#if ESP32_WROOM_32_LORA_1_CHANNEL // No reset line for this device as it isn't connected on SX127X
         int ledPinNumber = Gpio.IO17;
         int chipSelectPinNumber = Gpio.IO16;
#endif
#if NETDUINO3_WIFI
         int ledPinNumber  = PinNumber('A', 10);
         // Arduino D10->PB10
         int chipSelectPinNumber = PinNumber('B', 10);
         // Arduino D9->PE5
         int resetPinNumber = PinNumber('E', 5);
#endif
#if ST_NUCLEO64_F091RC // No LED for this device as driven by D13 the SPI CLK line
         // Arduino D10->PB6
         int chipSelectPinNumber = PinNumber('B', 6);
         // Arduino D9->PC7
         int resetPinNumber = PinNumber('C', 7);
#endif
#if ST_STM32F429I_DISCOVERY // No reset line for this device as I didn't bother with jumper to SX127X pin
         int ledPinNumber  = PinNumber('G', 14);
         int chipSelectPinNumber = PinNumber('C', 2);
#endif
#if ST_STM32F769I_DISCOVERY
         int ledPinNumber  = PinNumber('J', 5);
         // Arduino D10->PA11
         int chipSelectPinNumber = PinNumber('A', 11);
         // Arduino D9->PH6
         int resetPinNumber = PinNumber('H', 6);
#endif
         Debug.WriteLine("devMobile.IoT.Rfm9x.ShieldSPI starting");

         try
         {
            GpioController gpioController = GpioController.GetDefault();

#if NETDUINO3_WIFI|| ST_NUCLEO64_F091RC || ST_STM32F769I_DISCOVERY
            // Setup the reset pin
            GpioPin resetGpioPin = gpioController.OpenPin(resetPinNumber);
            resetGpioPin.SetDriveMode(GpioPinDriveMode.Output);
            resetGpioPin.Write(GpioPinValue.High);
#endif

#if ESP32_WROOM_32_LORA_1_CHANNEL || NETDUINO3_WIFI|| ST_STM32F429I_DISCOVERY || ST_STM32F769I_DISCOVERY
            // Setup the onboard LED
            GpioPin led = gpioController.OpenPin(ledPinNumber);
            led.SetDriveMode(GpioPinDriveMode.Output);
#endif

#if ESP32_WROOM_32_LORA_1_CHANNEL
            Configuration.SetPinFunction(nanoFramework.Hardware.Esp32.Gpio.IO12, DeviceFunction.SPI1_MISO);
            Configuration.SetPinFunction(nanoFramework.Hardware.Esp32.Gpio.IO13, DeviceFunction.SPI1_MOSI);
            Configuration.SetPinFunction(nanoFramework.Hardware.Esp32.Gpio.IO14, DeviceFunction.SPI1_CLOCK);
#endif

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

            using (SpiDevice device = SpiDevice.FromId(SpiBusId, settings))
            {
               Thread.Sleep(500);
            
               while (true)
               {
                  byte[] writeBuffer = new byte[] { RegVersion, 0x0 };
                  byte[] readBuffer = new byte[writeBuffer.Length];

                  device.TransferFullDuplex(writeBuffer, readBuffer);

                  Debug.WriteLine(String.Format("Register 0x{0:x2} - Value 0X{1:x2}", RegVersion, readBuffer[1]));

                  #if ESP32_WROOM_32_LORA_1_CHANNEL|| NETDUINO3_WIFI || ST_STM32F429I_DISCOVERY || ST_STM32F769I_DISCOVERY
                     led.Toggle();
                  #endif
                  Thread.Sleep(10000);
               }
            }
         }
         catch (Exception ex)
         {
            Debug.WriteLine(ex.Message);
         }
      }

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

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

In the Visual Studio output windows I could see the correct version register value

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.IoT.Rfm9x.ShieldSPI starting
Register 0x42 - Value 0X12
Register 0x42 - Value 0X12
...

After checking the configuration of the reset (D9) and interrupt (D2) pins in other test harness programs my final configuration for Rfm9xLoRaDevice client was

//---------------------------------------------------------------------------------
// Copyright (c) April/May 2020, 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 ADDRESSED_MESSAGES_PAYLOAD
//#define ESP32_WROOM_32_LORA_1_CHANNEL   //nanoff --target ESP32_WROOM_32 --serialport COM4 --update
#define NETDUINO3_WIFI   // nanoff --target NETDUINO3_WIFI --update
//#define ST_STM32F429I_DISCOVERY       //nanoff --target ST_STM32F429I_DISCOVERY --update
namespace devMobile.IoT.Rfm9x.LoRaDeviceClient
{
	using System;
	using System.Diagnostics;
	using System.Text;
	using System.Threading;

#if ESP32_WROOM_32_LORA_1_CHANNEL
	using nanoFramework.Hardware.Esp32;
#endif

	using devMobile.IoT.Rfm9x;

	class Program
	{
		private const double Frequency = 915000000.0;
#if ST_STM32F429I_DISCOVERY
		private const string DeviceName = "Disco429";
		private const string SpiBusId = "SPI5";
#endif
#if ESP32_WROOM_32_LORA_1_CHANNEL
		private const string DeviceName = "ESP32";
		private const string SpiBusId = "SPI1";
#endif
#if NETDUINO3_WIFI
		private const string DeviceName = "N3W";
		private const string SpiBusId = "SPI2";
#endif
#if ADDRESSED_MESSAGES_PAYLOAD
		private const string DeviceName = "LoRaIoT1";
#endif

		static void Main()
		{
			byte MessageCount = System.Byte.MaxValue;
#if ST_STM32F429I_DISCOVERY
			int chipSelectPinNumber = PinNumber('C', 2);
			int resetPinNumber = PinNumber('C', 3);
			int interruptPinNumber = PinNumber('A', 4);
#endif
#if ESP32_WROOM_32_LORA_1_CHANNEL
         int chipSelectPinNumber = Gpio.IO16;
         int interruptPinNumber = Gpio.IO26;

			Configuration.SetPinFunction(Gpio.IO12, DeviceFunction.SPI1_MISO);
			Configuration.SetPinFunction(Gpio.IO13, DeviceFunction.SPI1_MOSI);
			Configuration.SetPinFunction(Gpio.IO14, DeviceFunction.SPI1_CLOCK);

			Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SpiBusId, chipSelectPinNumber, interruptPinNumber);
#endif
#if NETDUINO3_WIFI
			// Arduino D10->PB10
			int chipSelectPinNumber = PinNumber('B', 10);
			// Arduino D9->PE5
			int resetPinNumber = PinNumber('E', 5);
			// Arduino D2->PA3
			int interruptPinNumber = PinNumber('A', 3);
#endif

#if ST_STM32F429I_DISCOVERY || NETDUINO3_WIFI
			Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SpiBusId, chipSelectPinNumber, resetPinNumber, interruptPinNumber);
#endif
			rfm9XDevice.Initialise(Frequency, paBoost: true);

#if DEBUG
			rfm9XDevice.RegisterDump();
#endif

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

			Thread.Sleep(10000);

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

				byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
				Debug.WriteLine(string.Format("{0}-TX {1} byte message {2}", DateTime.UtcNow.ToString("HH:mm:ss"), messageBytes.Length, messageText));
#if ADDRESSED_MESSAGES_PAYLOAD
				rfm9XDevice.Send(UTF8Encoding.UTF8.GetBytes(HostName), messageBytes);
#else
				rfm9XDevice.Send(messageBytes);
#endif
				Thread.Sleep(10000);
			}
		}

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

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

#if ADDRESSED_MESSAGES_PAYLOAD
				string addressText = UTF8Encoding.UTF8.GetString(e.Address, 0, e.Address.Length);

				Debug.WriteLine(string.Format(@"{0}-RX From {1} PacketSnr {2} Packet RSSI {3}dBm RSSI {4}dBm ={5} ""{6}""", DateTime.UtcNow.ToString("HH:mm:ss"), addressText, e.PacketSnr, e.PacketRssi, e.Rssi, e.Data.Length, messageText));
#else
				Debug.WriteLine(string.Format(@"{0}-RX PacketSnr {1} Packet RSSI {2}dBm RSSI {3}dBm ={4} ""{5}""", DateTime.UtcNow.ToString("HH:mm:ss"), e.PacketSnr, e.PacketRssi, e.Rssi, e.Data.Length, messageText));
#endif
			}
			catch (Exception ex)
			{
				Debug.WriteLine(ex.Message);
			}
		}

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

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

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


The sample client could reliable send and receive messages.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
Register dump
Register 0x00 - Value 0X7A
Register 0x01 - Value 0X80
Register 0x02 - Value 0X1A
Register 0x03 - Value 0X0B
Register 0x04 - Value 0X00
Register 0x05 - Value 0X52
Register 0x06 - Value 0XE4
Register 0x07 - Value 0XC0
Register 0x08 - Value 0X00
Register 0x09 - Value 0XCF
Register 0x0A - Value 0X09
Register 0x0B - Value 0X2B
Register 0x0C - Value 0X20
Register 0x0D - Value 0X01
Register 0x0E - Value 0X80
Register 0x0F - Value 0X00
Register 0x10 - Value 0X00
Register 0x11 - Value 0X00
Register 0x12 - Value 0X00
Register 0x13 - Value 0X00
Register 0x14 - Value 0X00
Register 0x15 - Value 0X00
Register 0x16 - Value 0X00
Register 0x17 - Value 0X00
Register 0x18 - Value 0X10
Register 0x19 - Value 0X00
Register 0x1A - Value 0X00
Register 0x1B - Value 0X00
Register 0x1C - Value 0X00
Register 0x1D - Value 0X72
Register 0x1E - Value 0X70
Register 0x1F - Value 0X64
Register 0x20 - Value 0X00
Register 0x21 - Value 0X08
Register 0x22 - Value 0X01
Register 0x23 - Value 0XFF
Register 0x24 - Value 0X00
Register 0x25 - Value 0X00
Register 0x26 - Value 0X04
Register 0x27 - Value 0X00
Register 0x28 - Value 0X00
Register 0x29 - Value 0X00
Register 0x2A - Value 0X00
Register 0x2B - Value 0X00
Register 0x2C - Value 0X00
Register 0x2D - Value 0X50
Register 0x2E - Value 0X14
Register 0x2F - Value 0X45
Register 0x30 - Value 0X55
Register 0x31 - Value 0XC3
Register 0x32 - Value 0X05
Register 0x33 - Value 0X27
Register 0x34 - Value 0X1C
Register 0x35 - Value 0X0A
Register 0x36 - Value 0X03
Register 0x37 - Value 0X0A
Register 0x38 - Value 0X42
Register 0x39 - Value 0X12
Register 0x3A - Value 0X49
Register 0x3B - Value 0X1D
Register 0x3C - Value 0X00
Register 0x3D - Value 0XAF
Register 0x3E - Value 0X00
Register 0x3F - Value 0X00
Register 0x40 - Value 0X00
Register 0x41 - Value 0X00
Register 0x42 - Value 0X12
00:00:25-TX 20 byte message Hello from N3W ! 255
00:00:25-TX Done
00:00:35-TX 20 byte message Hello from N3W ! 254
00:00:35-TX Done
00:00:45-TX 20 byte message Hello from N3W ! 253
00:00:45-TX Done
00:00:46-RX PacketSnr 9.50 Packet RSSI -70dBm RSSI -110dBm =59 " LoRaIoT1Maduino2at 43.9,ah 75,wsa 1,wsg 2,wd 36.00,r 0.00,"
00:00:55-TX 20 byte message Hello from N3W ! 252
00:00:55-TX Done
00:01:05-TX 20 byte message Hello from N3W ! 251
00:01:05-TX Done

Overall the process was fairly painless and helped identify a bug in the configuration of the Mode register in one of the test harness applications.

.Net Meadow nRF24L01 library Part3

While testing my initial port of the the techfooninja nRF24L01P library to a Wilderness Labs Meadow I noticed that the power level value was a bit odd.

nRF24L01P Test Harness
The program '[16720] App.exe' has exited with code 0 (0x0).
 IsPowered: True
 Address: Dev01
 PA: 15
 IsAutoAcknowledge: True
 Channel: 15
 DataRate: DR250Kbps
 Power: 15
 IsDynamicAcknowledge: False
 IsDynamicPayload: True
 IsEnabled: False
 Frequency: 2415
 IsInitialized: True
 IsPowered: True
 00:00:18-TX 8 byte message hello 17
 Data Sent!
00:00:18-TX Succeeded!
 00:00:48-TX 8 byte message hello 48
 Data Sent!

Looking at nRF24L01P datasheet and how this has been translated into code

/// <summary>
///   The power level for the radio.
/// </summary>
public PowerLevel PowerLevel
{
  get
   {
      var regValue = Execute(Commands.R_REGISTER, Registers.RF_SETUP, new byte[1])[1] & 0xF8;
      var newValue = (regValue - 1) >> 1;
      return (PowerLevel)newValue;
   }
  set
   {
      var regValue = Execute(Commands.R_REGISTER, Registers.RF_SETUP, new byte[1])[1] & 0xF8;

      byte newValue = (byte)((byte)value << 1 + 1);

      Execute(Commands.W_REGISTER, Registers.RF_SETUP,
              new[]
                  {
                     (byte) (newValue | regValue)
                  });
   }
}

The power level enumeration is declared in PowerLevel.cs

namespace Radios.RF24
{
    /// <summary>
    ///   Power levels the radio can operate with
    /// </summary>
    public enum PowerLevel : byte
    {
        /// <summary>
        ///   Minimum power setting for the radio
        /// </summary>
        Minimum = 0,

        /// <summary>
        ///   Low power setting for the radio
        /// </summary>
        Low,

        /// <summary>
        ///   High power setting for the radio
        /// </summary>
        High,

        /// <summary>
        ///   Max power setting for the radio
        /// </summary>
        Max,

        /// <summary>
        ///   Error with the power setting
        /// </summary>
        Error
    }
}

No debugging support or Debug.WriteLine in beta 3.7 (March 2020) so first step was to insert a Console.Writeline so I could see what the RF_SETUP register value was.

The program '[11212] App.exe' has exited with code 0 (0x0).
 Address: Dev01
 PowerLevel regValue 00100101
 PowerLevel: 15
 IsAutoAcknowledge: True
 Channel: 15
 DataRate: DR250Kbps
 IsDynamicAcknowledge: False
 IsDynamicPayload: True
 IsEnabled: False
 Frequency: 2415
 IsInitialized: True
 IsPowered: True
 00:00:18-TX 8 byte message hello 17
 Data Sent!
00:00:18-TX Succeeded!

The PowerLevel setting appeared to make no difference and the bits 5, 2 & 0 were set which meant 250Kbps & high power which I was expecting.

The RF_SETUP register in the datasheet, contains the following settings (WARNING – some nRF24L01 registers differ from nRF24L01P)

After looking at the code my initial “quick n dirty” fix was to mask out the existing power level bits and then mask in the new setting.

public PowerLevel PowerLevel
      {
         get
         {
            byte regValue = Execute(Commands.R_REGISTER, Registers.RF_SETUP, new byte[1])[1];;
            Console.WriteLine($"PowerLevel regValue {Convert.ToString(regValue, 2).PadLeft(8, '0')}");
            var newValue = (regValue & 0x06) >> 1;
            
            return (PowerLevel)newValue;
         }
         set
         {
            byte regValue = Execute(Commands.R_REGISTER, Registers.RF_SETUP, new byte[1])[1];
            regValue &= 0b11111000;
            regValue |= (byte)((byte)value << 1);

            Execute(Commands.W_REGISTER, Registers.RF_SETUP,
                    new[]
                        {
                            (byte)regValue
                        });
         }
      }

I wonder if the code mighty be simpler if I used a similar approach to my Windows 10 IoT RFM9X LoRa library

// RegModemConfig1
public enum RegModemConfigBandwidth : byte
{
	_7_8KHz = 0b00000000,
	_10_4KHz = 0b00010000,
	_15_6KHz = 0b00100000,
	_20_8KHz = 0b00110000,
	_31_25KHz = 0b01000000,
	_41_7KHz = 0b01010000,
	_62_5KHz = 0b01100000,
	_125KHz = 0b01110000,
	_250KHz = 0b10000000,
	_500KHz = 0b10010000
}
public const RegModemConfigBandwidth RegModemConfigBandwidthDefault = RegModemConfigBandwidth._125KHz;

...

[Flags]
enum RegIrqFlagsMask : byte
{
	RxTimeoutMask = 0b10000000,
	RxDoneMask = 0b01000000,
	PayLoadCrcErrorMask = 0b00100000,
	ValidHeadrerMask = 0b00010000,
	TxDoneMask = 0b00001000,
	CadDoneMask = 0b00000100,
	FhssChangeChannelMask = 0b00000010,
	CadDetectedMask = 0b00000001,
}

[Flags]
enum RegIrqFlags : byte
{
	RxTimeout = 0b10000000,
	RxDone = 0b01000000,
	PayLoadCrcError = 0b00100000,
	ValidHeadrer = 0b00010000,
	TxDone = 0b00001000,
	CadDone = 0b00000100,
	FhssChangeChannel = 0b00000010,
	CadDetected = 0b00000001,
	ClearAll = 0b11111111,
}

This would require some significant modifications to the Techfooninja library. e.g. the PowerLevel enumeration

namespace Radios.RF24
{
    /// <summary>
    ///   Power levels the radio can operate with
    /// </summary>
    public enum PowerLevel : byte
    {
        /// <summary>
        ///   Minimum power setting for the radio
        /// </summary>
        Minimum = 0b00000000,

        /// <summary>
        ///   Low power setting for the radio
        /// </summary>
        Low = 0b00000010,

        /// <summary>
        ///   High power setting for the radio
        /// </summary>
        High = 0b00000100,

        /// <summary>
        ///   Max power setting for the radio
        /// </summary>
        Max = 0b00000110,
    }
}

I need to do some more testing of the of library to see if the pattern is repeated.

Wilderness Labs nRF24L01 Wireless field gateway Meadow client

After a longish pause in development work on my nrf24L01 AdaFruit.IO and Azure IOT Hub field gateways I figured a client based on my port of the techfooninja nRF24 library to Wilderness Labs Meadow would be a good test.

This sample client is an Wilderness Labs Meadow with a Sensiron SHT31 Temperature & humidity sensor (supported by meadow foundation), and a generic nRF24L01 device connected with jumper cables.

Bill of materials (prices as at March 2020)

  • Wilderness Labs Meadow 7F Micro device USD50
  • Seeedstudio Temperature and Humidity Sensor(SHT31) USD11.90
  • Seeedstudio 4 pin Male Jumper to Grove 4 pin Conversion Cable USD2.90
  • 2.4G Wireless Module nRF24L01+PA USD9.90

The initial version of the code was pretty basic with limited error handling and no power conservation support.

namespace devMobile.IoT.FieldGateway.Client
{
   using System;
   using System.Text;
   using System.Threading;

   using Radios.RF24;

   using Meadow;
   using Meadow.Devices;
   using Meadow.Foundation.Leds;
   using Meadow.Foundation.Sensors.Atmospheric;
   using Meadow.Hardware;
   using Meadow.Peripherals.Leds;

   public class MeadowClient : App<F7Micro, MeadowClient>
   {
      private const string BaseStationAddress = "Base1";
      private const string DeviceAddress = "WLAB1";
      private const byte nRF24Channel = 15;
      private RF24 Radio = new RF24();
      private readonly TimeSpan periodTime = new TimeSpan(0, 0, 60);
      private readonly Sht31D sensor;
      private readonly ILed Led;

      public MeadowClient()
      {
         Led = new Led(Device, Device.Pins.OnboardLedGreen);

         try
         {
            sensor = new Sht31D(Device.CreateI2cBus());

            var config = new Meadow.Hardware.SpiClockConfiguration(
                           2000,
                           SpiClockConfiguration.Mode.Mode0);

            ISpiBus spiBus = Device.CreateSpiBus(
               Device.Pins.SCK,
               Device.Pins.MOSI,
               Device.Pins.MISO, config);

            Radio.OnDataReceived += Radio_OnDataReceived;
            Radio.OnTransmitFailed += Radio_OnTransmitFailed;
            Radio.OnTransmitSuccess += Radio_OnTransmitSuccess;

            Radio.Initialize(Device, spiBus, Device.Pins.D09, Device.Pins.D10, Device.Pins.D11);
            //Radio.Address = Encoding.UTF8.GetBytes(Environment.MachineName);
            Radio.Address = Encoding.UTF8.GetBytes(DeviceAddress);

            Radio.Channel = nRF24Channel;
            Radio.PowerLevel = PowerLevel.Low;
            Radio.DataRate = DataRate.DR250Kbps;
            Radio.IsEnabled = true;

            Radio.IsAutoAcknowledge = true;
            Radio.IsDyanmicAcknowledge = false;
            Radio.IsDynamicPayload = true;

            Console.WriteLine($"Address: {Encoding.UTF8.GetString(Radio.Address)}");
            Console.WriteLine($"PowerLevel: {Radio.PowerLevel}");
            Console.WriteLine($"IsAutoAcknowledge: {Radio.IsAutoAcknowledge}");
            Console.WriteLine($"Channel: {Radio.Channel}");
            Console.WriteLine($"DataRate: {Radio.DataRate}");
            Console.WriteLine($"IsDynamicAcknowledge: {Radio.IsDyanmicAcknowledge}");
            Console.WriteLine($"IsDynamicPayload: {Radio.IsDynamicPayload}");
            Console.WriteLine($"IsEnabled: {Radio.IsEnabled}");
            Console.WriteLine($"Frequency: {Radio.Frequency}");
            Console.WriteLine($"IsInitialized: {Radio.IsInitialized}");
            Console.WriteLine($"IsPowered: {Radio.IsPowered}");
         }
         catch (Exception ex)
         {
            Console.WriteLine(ex.Message);
         }

         while (true)
         {
            sensor.Update();

            Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-TX T:{sensor.Temperature:0.0}C H:{sensor.Humidity:0}%");

            Led.IsOn = true;

            string values = "T " + sensor.Temperature.ToString("F1") + ",H " + sensor.Humidity.ToString("F0");

            // Stuff the 2 byte header ( payload type & deviceIdentifierLength ) + deviceIdentifier into payload
            byte[] payload = new byte[1 + Radio.Address.Length + values.Length];
            payload[0] = (byte)((1 << 4) | Radio.Address.Length);
            Array.Copy(Radio.Address, 0, payload, 1, Radio.Address.Length);
            Encoding.UTF8.GetBytes(values, 0, values.Length, payload, Radio.Address.Length + 1);

            Radio.SendTo(Encoding.UTF8.GetBytes(BaseStationAddress), payload);

            Thread.Sleep(periodTime);
         }
      }

      private void Radio_OnDataReceived(byte[] data)
      {
         // Display as Unicode
         string unicodeText = Encoding.UTF8.GetString(data);
         Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-RX Unicode Length {0} Unicode Length {1} Unicode text {2}", data.Length, unicodeText.Length, unicodeText);

         // display as hex
         Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-RX Hex Length {data.Length} Payload {BitConverter.ToString(data)}");
      }

      private void Radio_OnTransmitSuccess()
      {
         Led.IsOn = false;

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

      private void Radio_OnTransmitFailed()
      {
         Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-TX failed!");
      }
   }
}

After sorting out power to the SHT31 (I had to push the jumper cable further into the back of the jumper cable plug). I could see temperature and humidity values getting uploaded to Adafruit.IO.

Visual Studio 2019 debug output

Adafruit.IO “automagically” provisions new feeds which is helpful when building a proof of concept (PoC)

Adafruit.IO feed with default feed IDs

I then modified the feed configuration to give it a user friendly name.

Feed Configuration

All up configuration took about 10 minutes.

Meadow device temperature and humidity

.Net Meadow nRF24L01 library Part2

After getting SPI connectivity going my next step porting the techfooninja nRF24L01P library to a Wilderness Labs Meadow was rewriting the SPI port initialisation, plus GetStatus and Execute methods.

nRF24L01P Test Harness

I added a digital output port for the Chip Select and because I can specify the interrupt trigger edge I removed the test from the interrupt handler.

 public void Initialize(IIODevice device, ISpiBus spiBus, IPin chipEnablePin, IPin chipSelectLine, IPin interruptPin)
{
   _SpiBus = spiBus;

   _cePin = device.CreateDigitalOutputPort(chipEnablePin, false);

   _csPin = device.CreateDigitalOutputPort(chipSelectLine, false);

   _irqPin = device.CreateDigitalInputPort(interruptPin, InterruptMode.EdgeFalling, resistorMode: ResistorMode.PullUp);
   _irqPin.Changed += InterruptGpioPin_ValueChanged;

   // Module reset time
   Task.Delay(100).GetAwaiter().GetResult();

   IsInitialized = true;

   // Set reasonable default values
   Address = Encoding.UTF8.GetBytes("NRF1");
   DataRate = DataRate.DR2Mbps;
   IsDynamicPayload = true;
   IsAutoAcknowledge = true;

   FlushReceiveBuffer();
   FlushTransferBuffer();
   ClearIrqMasks();
   SetRetries(5, 60);

   // Setup, CRC enabled, Power Up, PRX
   SetReceiveMode();
}

The core of the Initialise method was moved to the Meadow application startup.

public MeadowApp()
{
   try
   {
		var config = new Meadow.Hardware.SpiClockConfiguration(
			2000,
			SpiClockConfiguration.Mode.Mode0);

		ISpiBus spiBus = Device.CreateSpiBus(
			Device.Pins.SCK,
			Device.Pins.MOSI,
			Device.Pins.MISO,config);

		Radio.OnDataReceived += Radio_OnDataReceived;
		Radio.OnTransmitFailed += Radio_OnTransmitFailed;
		Radio.OnTransmitSuccess += Radio_OnTransmitSuccess;

		Radio.Initialize(Device, spiBus, Device.Pins.D09, Device.Pins.D10, Device.Pins.D11);
		Radio.Address = Encoding.UTF8.GetBytes(DeviceAddress);

		Radio.Channel = nRF24Channel;
		Radio.PowerLevel = PowerLevel.High;
		Radio.DataRate = DataRate.DR250Kbps;
		Radio.IsEnabled = true;

		Radio.IsAutoAcknowledge = true;
		Radio.IsDyanmicAcknowledge = false;
		Radio.IsDynamicPayload = true;

		Console.WriteLine($"Address: {Encoding.UTF8.GetString(Radio.Address)}");
		Console.WriteLine($"PA: {Radio.PowerLevel}");
		Console.WriteLine($"IsAutoAcknowledge: {Radio.IsAutoAcknowledge}");
		Console.WriteLine($"Channel: {Radio.Channel}");
		Console.WriteLine($"DataRate: {Radio.DataRate}");
		Console.WriteLine($"Power: {Radio.PowerLevel}");
		Console.WriteLine($"IsDynamicAcknowledge: {Radio.IsDyanmicAcknowledge}");
		Console.WriteLine($"IsDynamicPayload: {Radio.IsDynamicPayload}");
		Console.WriteLine($"IsEnabled: {Radio.IsEnabled}");
		Console.WriteLine($"Frequency: {Radio.Frequency}");
		Console.WriteLine($"IsInitialized: {Radio.IsInitialized}");
		Console.WriteLine($"IsPowered: {Radio.IsPowered}");
	}
	catch (Exception ex)
	{
		Console.WriteLine(ex.Message);

		return;
	}

I modified the GetStatus and ExecuteMethods to use the ExchangeData method

   /// <summary>
      ///   Executes a command in NRF24L01+ (for details see module datasheet)
      /// </summary>
      /// <param name = "command">Command</param>
      /// <param name = "addres">Register to write to or read from</param>
      /// <param name = "data">Data to write or buffer to read to</param>
      /// <returns>Response byte array. First byte is the status register</returns>
      public byte[] Execute(byte command, byte addres, byte[] data)
      {
         CheckIsInitialized();

         // This command requires module to be in power down or standby mode
         if (command == Commands.W_REGISTER)
            IsEnabled = false;

         // Create SPI Buffers with Size of Data + 1 (For Command)
         var writeBuffer = new byte[data.Length + 1];
         var readBuffer = new byte[data.Length + 1];

         // Add command and address to SPI buffer
         writeBuffer[0] = (byte)(command | addres);

         // Add data to SPI buffer
         Array.Copy(data, 0, writeBuffer, 1, data.Length);

         // Do SPI Read/Write
         _SpiBus.ExchangeData(_csPin, ChipSelectMode.ActiveLow, writeBuffer, readBuffer);

         // Enable module back if it was disabled
         if (command == Commands.W_REGISTER && _enabled)
            IsEnabled = true;

         // Return ReadBuffer
         return readBuffer;
      }

      /// <summary>
      ///   Gets module basic status information
      /// </summary>
      /// <returns>Status object representing the current status of the radio</returns>
      public Status GetStatus()
      {
         CheckIsInitialized();

         var readBuffer = new byte[1];
         _SpiBus.ExchangeData(_csPin, ChipSelectMode.ActiveLow, new[] { Commands.NOP }, readBuffer);

         return new Status(readBuffer[0]);
      }

After these modifications I can send and receive messages but the PowerLevel doesn’t look right.

The program '[16720] App.exe' has exited with code 0 (0x0).
 IsPowered: True
 Address: Dev01
 PA: 15
 IsAutoAcknowledge: True
 Channel: 15
 DataRate: DR250Kbps
 Power: 15
 IsDynamicAcknowledge: False
 IsDynamicPayload: True
 IsEnabled: False
 Frequency: 2415
 IsInitialized: True
 IsPowered: True
 00:00:18-TX 8 byte message hello 17
 Data Sent!
00:00:18-TX Succeeded!
 00:00:48-TX 8 byte message hello 48
 Data Sent!

Time to dig into the nRF24L01P datasheet.

.Net Meadow nRF24L01 library Part1

After debugging Windows 10 IoT Core & .NetMF nRF24L01P libraries I figured a port to a Wilderness Labs Meadow device shouldn’t be “rocket science”.

I couldn’t source an nRF24L01 feather wing so built a test rig with jumpers

nRF24L01P Test Harness
//---------------------------------------------------------------------------------
// Copyright (c) Feb 2020, devMobile Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//---------------------------------------------------------------------------------
namespace devMobile.IoT.nRf24L01
{
   using System;
   using System.Text;
   using System.Threading;
   using Meadow;
   using Meadow.Devices;
   using Meadow.Hardware;

   public class MeadowApp : App<F7Micro, MeadowApp>
   {
      const byte SETUP_AW = 0x03;
      const byte RX_ADDR_P0 = 0x0A;
      const byte R_REGISTER = 0b00000000;
      const byte W_REGISTER = 0b00100000;
      ISpiBus spiBus;
      SpiPeripheral nrf24L01Device;
      IDigitalOutputPort spiPeriphChipSelect;
      IDigitalOutputPort ChipEnable;


      public MeadowApp()
      {
         ConfigureSpiPort();
         SetPipe0RxAddress("ZYXWV");
      }

      public void ConfigureSpiPort()
      {
         try
         {
            ChipEnable = Device.CreateDigitalOutputPort(Device.Pins.D09, initialState: false);
            if (ChipEnable == null)
            {
               Console.WriteLine("chipEnable == null");
            }

            var spiClockConfiguration = new SpiClockConfiguration(2000, SpiClockConfiguration.Mode.Mode0);
            spiBus = Device.CreateSpiBus(Device.Pins.SCK,
                                         Device.Pins.MOSI,
                                         Device.Pins.MISO,
                                         spiClockConfiguration);
            if (spiBus == null)
            {
               Console.WriteLine("spiBus == null");
            }

            Console.WriteLine("Creating SPI NSS Port...");
            spiPeriphChipSelect = Device.CreateDigitalOutputPort(Device.Pins.D10, initialState: true);
            if (spiPeriphChipSelect == null)
            {
               Console.WriteLine("spiPeriphChipSelect == null");
            }

            Console.WriteLine("nrf24L01Device Device...");
            nrf24L01Device = new SpiPeripheral(spiBus, spiPeriphChipSelect);
            if (nrf24L01Device == null)
            {
               Console.WriteLine("nrf24L01Device == null");
            }

            Thread.Sleep(100);

            Console.WriteLine("ConfigureSpiPort Done...");
         }
         catch (Exception ex)
         {
            Console.WriteLine("ConfigureSpiPort " + ex.Message);
         }
      }

      public void SetPipe0RxAddress(string address)
      {
         try
         {
            // Read the Address width
            byte[] txBuffer1 = new byte[] { SETUP_AW | R_REGISTER, 0x0 };
            Console.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer1));

            /*
            // Appears to work but not certain it does
            Console.WriteLine(" nrf24L01Device.WriteRead...SETUP_AW");
            byte[] rxBuffer1 = nrf24L01Device.WriteRead(txBuffer1, (ushort)txBuffer1.Length);
            Console.WriteLine(" nrf24L01Device.WriteRead...SETUP_AW");
            */

            byte[] rxBuffer1 = new byte[txBuffer1.Length];
            Console.WriteLine(" spiBus.ExchangeData...RX_ADDR_P0");
            spiBus.ExchangeData(spiPeriphChipSelect, ChipSelectMode.ActiveLow, txBuffer1, rxBuffer1);

            Console.WriteLine(" rxBuffer:" + BitConverter.ToString(rxBuffer1));

            // Extract then adjust the address width
            byte addressWidthValue = rxBuffer1[1];
            addressWidthValue &= 0b00000011;
            addressWidthValue += 2;
            Console.WriteLine("Address width 0x{0:x2} - Value 0X{1:x2} - Bits {2} Value adjusted {3}", SETUP_AW, rxBuffer1[1], Convert.ToString(rxBuffer1[1], 2).PadLeft(8, '0'), addressWidthValue);
            Console.WriteLine();

            // Write Pipe0 Receive address
            Console.WriteLine("Address write 1");
            byte[] txBuffer2 = new byte[addressWidthValue + 1];
            txBuffer2[0] = RX_ADDR_P0 | W_REGISTER;
            Array.Copy(Encoding.UTF8.GetBytes(address), 0, txBuffer2, 1, addressWidthValue);
            Console.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer2));

            Console.WriteLine(" nrf24L01Device.Write...RX_ADDR_P0");
            nrf24L01Device.WriteBytes(txBuffer2);
            Console.WriteLine();

            // Read Pipe0 Receive address
            Console.WriteLine("Address read 1");
            byte[] txBuffer3 = new byte[addressWidthValue + 1];
            txBuffer3[0] = RX_ADDR_P0 | R_REGISTER;
            Console.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer3));

            /*
            // Broken returns  Address 0x0a - RX Buffer 5A-5A-5A-5A-59-58 RX Address 5A-5A-5A-59-58 Address ZZZYX
            Console.WriteLine(" nrf24L01Device.WriteRead...RX_ADDR_P0");
            byte[] rxBuffer3 = nrf24L01Device.WriteRead(txBuffer3, (ushort)txBuffer3.Length);
            */

            byte[] rxBuffer3 = new byte[addressWidthValue + 1];
            Console.WriteLine(" spiBus.ExchangeData...RX_ADDR_P0");
            spiBus.ExchangeData(spiPeriphChipSelect, ChipSelectMode.ActiveLow, txBuffer3, rxBuffer3);

            Console.WriteLine("Address 0x{0:x2} - RX Buffer {1} RX Address {2} Address {3}", RX_ADDR_P0, BitConverter.ToString(rxBuffer3, 0), BitConverter.ToString(rxBuffer3, 1), UTF8Encoding.UTF8.GetString(rxBuffer3, 1, addressWidthValue));
         }
         catch (Exception ex)
         {
            Console.WriteLine("ReadDeviceIDDiy " + ex.Message);
         }
      }
   }
}

After lots of tinkering with SPI configuration options and trialing different methods (spiBus vs.SpiPeripheral) I can read and write my nRF24L01 device receive port address

 Creating SPI NSS Port...
 nrf24L01Device Device...
 ConfigureSpiPort Done...
  txBuffer:03-00
  spiBus.ExchangeData...RX_ADDR_P0
  rxBuffer:0E-03
 Address width 0x03 - Value 0X03 - Bits 00000011 Value adjusted 5
 
 Address write 1
  txBuffer:2A-5A-59-58-57-56
  nrf24L01Device.Write...RX_ADDR_P0
 
 Address read 1
  txBuffer:0A-00-00-00-00-00
  spiBus.ExchangeData...RX_ADDR_P0
 Address 0x0a - RX Buffer 0E-5A-59-58-57-56 RX Address 5A-59-58-57-56 Address ZYXWV

I need to investigate why the first byte of the buffer returned by nrf24L01Device.ReadBytes and nrf24L01Device.WriteRead is wrong.

Meadow LoRa Radio 915 MHz Payload Addressing client

This is a demo Wilderness Labs Meadow client that uploads temperature and humidity data to my Azure IoT Hubs/Central, AdaFruit.IO or MQTT on Raspberry PI field gateways.

Bill of materials (Prices Jan 2020).

//---------------------------------------------------------------------------------
// Copyright (c) January 2020, devMobile Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//---------------------------------------------------------------------------------
namespace devMobile.IoT.FieldGateway.Client
{
   using System;
   using System.Text;
   using System.Threading;

   using devMobile.IoT.Rfm9x;

   using Meadow;
   using Meadow.Devices;
   using Meadow.Foundation.Leds;
   using Meadow.Foundation.Sensors.Atmospheric;
   using Meadow.Hardware;
   using Meadow.Peripherals.Leds;

   public class MeadowClient : App<F7Micro, MeadowClient>
   {
      private const double Frequency = 915000000.0;
      private readonly byte[] fieldGatewayAddress = Encoding.UTF8.GetBytes("LoRaIoT1");
      private readonly byte[] deviceAddress = Encoding.UTF8.GetBytes("Meadow");
      private readonly Rfm9XDevice rfm9XDevice;
      private readonly TimeSpan periodTime = new TimeSpan(0, 0, 60);
      private readonly Sht31D sensor;
      private readonly ILed Led;

      public MeadowClient()
      {
         Led = new Led(Device, Device.Pins.OnboardLedGreen);

         try
         {
            sensor = new Sht31D(Device.CreateI2cBus());

            ISpiBus spiBus = Device.CreateSpiBus(500);

            rfm9XDevice = new Rfm9XDevice(Device, spiBus, Device.Pins.D09, Device.Pins.D10, Device.Pins.D12);

            rfm9XDevice.Initialise(Frequency, paBoost: true, rxPayloadCrcOn: true);
#if DEBUG
            rfm9XDevice.RegisterDump();
#endif
            rfm9XDevice.OnReceive += Rfm9XDevice_OnReceive;
            rfm9XDevice.Receive(deviceAddress);
            rfm9XDevice.OnTransmit += Rfm9XDevice_OnTransmit;
         }
         catch (Exception ex)
         {
            Console.WriteLine(ex.Message);
         }

         while (true)
         {
            sensor.Update();

            Console.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-TX T:{sensor.Temperature:0.0}C H:{sensor.Humidity:0}%");

            string payload = $"t {sensor.Temperature:0.0},h {sensor.Humidity:0}";

            Led.IsOn = true;

            rfm9XDevice.Send(fieldGatewayAddress, Encoding.UTF8.GetBytes(payload));

            Thread.Sleep(periodTime);
         }
      }

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

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

      private void Rfm9XDevice_OnTransmit(object sender, Rfm9XDevice.OnDataTransmitedEventArgs e)
      {
         Led.IsOn = false;

         Console.WriteLine("{0:HH:mm:ss}-TX Done", DateTime.Now);
      }
   }
}

The Meadow platform is a work in progress (Jan 2020) so I haven’t put any effort into minimising power consumption but will revisit this in a future post.

Meadow device with Seeedstudio SHT31 temperature & humidity sensor
Meadow sensor data in Field Gateway ETW logging
Meadow Sensor data in Azure IoT Central

RFM9X.Meadow on Github

After a month or so of posts the source code of V1.0 of my Wilderness Labs Meadow RFM9X/SX127X library is on GitHub. I included all of the source for my test harness and proof of concept(PoC) application so other people can follow along with “my meadow learning experience”.

I initially started with a Dragino LoRa Shield for Arduino and jumper cables. I did this so only the pins I was using on the shield were connected to the Meadow.

Dragino LoRa Shield for Arduino based test harness

I then moved to an Adafruit LoRa Radio FeatherWing RFM95W 900MHz RadioFruit and Adafruit LoRa Radio FeatherWing – RFM95W 433 MHz – RadioFruit.

Adafruit FeatherWing based test harness

Using the jumper configuration above, the RFM9X constructor parameters are

  • Chip Select D9 (yellow wire)
  • Reset Pin D10 (grey wire)
  • Interrupt pin D12 (brown wire)
//---------------------------------------------------------------------------------
// Copyright (c) January 2020, devMobile Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//---------------------------------------------------------------------------------
namespace devMobile.IoT.Rfm9x.LoRaDeviceClient
{
   using System;
   using System.Diagnostics;
   using System.Text;
   using System.Threading.Tasks;

   using Meadow;
   using Meadow.Devices;
   using Meadow.Hardware;

   public class MeadowApp : App<F7Micro, MeadowApp>
   {
      private const double Frequency = 915000000.0;
      private byte MessageCount = Byte.MaxValue;
      private Rfm9XDevice rfm9XDevice;

      public MeadowApp()
      {
         try
         {
            ISpiBus spiBus = Device.CreateSpiBus(500);
            if (spiBus == null)
            {
               Console.WriteLine("spiBus == null");
            }
            rfm9XDevice = new Rfm9XDevice(Device, spiBus, Device.Pins.D09, Device.Pins.D10, Device.Pins.D12);

            rfm9XDevice.Initialise(Frequency, paBoost: true, rxPayloadCrcOn: true);
#if DEBUG
            rfm9XDevice.RegisterDump();
#endif

            rfm9XDevice.OnReceive += Rfm9XDevice_OnReceive;
#if ADDRESSED_MESSAGES_PAYLOAD
            rfm9XDevice.Receive(UTF8Encoding.UTF8.GetBytes("AddressHere"));
#else
            rfm9XDevice.Receive();
#endif
            rfm9XDevice.OnTransmit += Rfm9XDevice_OnTransmit;
         }
         catch (Exception ex)
         {
            Console.WriteLine(ex.Message);
         }

         Task.Delay(10000).Wait();

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

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

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

#if ADDRESSED_MESSAGES_PAYLOAD
            string addressText = UTF8Encoding.UTF8.GetString(e.Address);

            Console.WriteLine(@"{0:HH:mm:ss}-RX From {1} PacketSnr {2:0.0} Packet RSSI {3}dBm RSSI {4}dBm = {5} byte message ""{6}""", DateTime.Now, addressText, e.PacketSnr, e.PacketRssi, e.Rssi, e.Data.Length, messageText);
#else
            Console.WriteLine(@"{0:HH:mm:ss}-RX PacketSnr {1:0.0} Packet RSSI {2}dBm RSSI {3}dBm = {4} byte message ""{5}""", DateTime.Now, e.PacketSnr, e.PacketRssi, e.Rssi, e.Data.Length, messageText);
#endif
         }
         catch (Exception ex)
         {
            Console.WriteLine(ex.Message);
         }
      }

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

My library works but some issues (Dec 2019) with the Serial Peripheral Interface (SPI) ReadRegister method, Debug.WriteLine and Console.WriteLine mean it should be treated as late beta.

The wilderness labs developers are regularly releasing updates which I will test with as soon as they are available.

.Net Meadow RFM95/96/97/98 LoRa library Part8

Transmit and Receive with Interrupts

For the final iteration of the “nasty” test harness I got the interrupts working for the transmitting and receiving of messages. It’s not quite simultaneous, the code sends a message every 10 seconds then goes back to receive continuous mode after each message has been sent.

      public Rfm9XDevice(IIODevice device, ISpiBus spiBus, IPin chipSelectPin, IPin resetPin, IPin interruptPin)
      {
         // Chip select pin configuration
         ChipSelectGpioPin = device.CreateDigitalOutputPort(chipSelectPin, initialState: true);
         if (ChipSelectGpioPin == null)
         {
            Console.WriteLine("ChipSelectGpioPin == null");
         }

         // Factory reset pin configuration
         IDigitalOutputPort resetGpioPin = device.CreateDigitalOutputPort(resetPin);
         if (resetGpioPin == null)
         {
            Console.WriteLine("resetGpioPin == null");
         }
         resetGpioPin.State = false;
         Task.Delay(10);
         resetGpioPin.State = true;
         Task.Delay(10);

         // Interrupt pin for RX message & TX done notification 
         InterruptGpioPin = device.CreateDigitalInputPort(interruptPin, InterruptMode.EdgeRising);
         InterruptGpioPin.Changed += InterruptGpioPin_ValueChanged;

         Rfm9XLoraModem = new SpiPeripheral(spiBus, ChipSelectGpioPin);
         if (Rfm9XLoraModem == null)
         {
            Console.WriteLine("Rfm9XLoraModem == null");
         }
      }

      private void InterruptGpioPin_ValueChanged(object sender, DigitalInputPortEventArgs args)
      {
         byte irqFlags = this.RegisterReadByte(0x12); // RegIrqFlags
         byte numberOfBytes = 0;
         string messageText = "";
         bool transmitDone = false;

         //Console.WriteLine(string.Format("RegIrqFlags:{0}", Convert.ToString(irqFlags, 2).PadLeft(8, '0')));
         if ((irqFlags & 0b01000000) == 0b01000000)
         {
            //Console.WriteLine("Receive-Message");
            byte currentFifoAddress = this.RegisterReadByte(0x10); // RegFifiRxCurrent
            this.RegisterWriteByte(0x0d, currentFifoAddress); // RegFifoAddrPtr

            numberOfBytes = this.RegisterReadByte(0x13); // RegRxNbBytes
            byte[] messageBytes = this.RegisterRead(0x00, numberOfBytes); // RegFifo
            messageText = UTF8Encoding.UTF8.GetString(messageBytes);
         }

         if ((irqFlags & 0b00001000) == 0b00001000)  // TxDone
         {
            this.RegisterWriteByte(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous
            transmitDone = true;
         }

         this.RegisterWriteByte(0x40, 0b00000000); // RegDioMapping1 0b00000000 DI0 RxReady & TxReady
         this.RegisterWriteByte(0x12, 0xff);// RegIrqFlags
         if (numberOfBytes > 0)
         {
            Console.WriteLine("Received {0} byte message {1}", numberOfBytes, messageText);
         }
         if(transmitDone)
         {
            Console.WriteLine("Transmit-Done");
         }
      }
...
   public class MeadowApp : App<F7Micro, MeadowApp>
   {
      private Rfm9XDevice rfm9XDevice;
      private byte NessageCount = Byte.MaxValue;

      public MeadowApp()
      {
         ISpiBus spiBus = Device.CreateSpiBus(500);
         if (spiBus == null)
         {
            Console.WriteLine("spiBus == null");
         }

         rfm9XDevice = new Rfm9XDevice(Device, spiBus, Device.Pins.D09, Device.Pins.D11, Device.Pins.D10);

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

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

         rfm9XDevice.RegisterWriteByte(0x0F, 0x0); // RegFifoRxBaseAddress 

         rfm9XDevice.RegisterWriteByte(0x09, 0b10000000); // RegPaConfig

         rfm9XDevice.RegisterWriteByte(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous

         while (true)
         {
            rfm9XDevice.RegisterWriteByte(0x0E, 0x0); // RegFifoTxBaseAddress 

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

            string messageText = "W10 IoT Core LoRa! " + NessageCount.ToString();
            NessageCount -= 1;

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

            // Set the length of the message in the fifo
            rfm9XDevice.RegisterWriteByte(0x22, (byte)messageBytes.Length); // RegPayloadLength
            Console.WriteLine("Sending {0} bytes message {1}", messageBytes.Length, messageText);
            rfm9XDevice.RegisterWriteByte(0x40, 0b01000000); // RegDioMapping1 0b00000000 DI0 RxReady & TxReady
            rfm9XDevice.RegisterWriteByte(0x01, 0b10000011); // RegOpMode 

            Debug.WriteLine("Sending {0} bytes message {1}", messageBytes.Length, messageText);

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

The diagnostic output shows inbound and outbound messages (Arduino then Meadow diagnostics)

21:15:56.070 -> 0x1: 0x81
21:15:56.070 -> 0x2: 0x1A
…
21:15:57.527 -> 0x7E: 0x0
21:15:57.527 -> 0x7F: 0x0
21:15:57.527 -> LoRa init succeeded.
21:15:58.101 -> Sending HeLoRa World! 0
21:16:06.958 -> Message: W10 IoT Core LoRa! 254
21:16:06.991 -> RSSI: -60
21:16:06.991 -> Snr: 9.50
21:16:06.991 ->
21:16:09.062 -> Sending HeLoRa World! 2
21:16:17.184 -> Message: W10 IoT Core LoRa! 253
21:16:17.218 -> RSSI: -61
21:16:17.218 -> Snr: 9.75
21:16:17.218 ->
21:16:19.876 -> Sending HeLoRa World! 4
21:16:21.946 -> Message: ⸮LoRaIoT1Maduino2at 65.3,ah 70,wsa 6,wsg 12,wd 167.25,r 0.00,
21:16:22.014 -> RSSI: -76
21:16:22.014 -> Snr: 9.50
21:16:22.014 ->
21:16:27.406 -> Message: W10 IoT Core LoRa! 252
21:16:27.429 -> RSSI: -60
21:16:27.429 -> Snr: 9.50
21:16:27.429 ->
21:16:30.153 -> Sending HeLoRa World! 6
'App.exe' (CLR v4.0.30319: DefaultDomain): Loaded 'C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll'. 
'App.exe' (CLR v4.0.30319: DefaultDomain): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.Meadow\ReceiveTransmitInterrupt\bin\Debug\net472\App.exe'. Symbols loaded.
'App.exe' (CLR v4.0.30319: App.exe): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.Meadow\ReceiveTransmitInterrupt\bin\Debug\net472\Meadow.dll'. 
The program '[42104] App.exe: Program Trace' has exited with code 0 (0x0).
The program '[42104] App.exe' has exited with code 0 (0x0).
 .
 .
 DirectRegisterAccess = True
 .
 .
 Sending 22 bytes message W10 IoT Core LoRa! 255
 Transmit-Done
 Received 15 byte message HeLoRa World! 0
 Sending 22 bytes message W10 IoT Core LoRa! 254
 Transmit-Done
 Received 15 byte message HeLoRa World! 2
 Sending 22 bytes message W10 IoT Core LoRa! 253
 Transmit-Done
 Received 15 byte message HeLoRa World! 4
 Received 61 byte message ???LoRaIoT1Maduino2at 65.3,ah 70,wsa 6,wsg 12,wd 167.25,r 0.00,
 Sending 22 bytes message W10 IoT Core LoRa! 252
 Transmit-Done
 Received 15 byte message HeLoRa World! 6
 Sending 22 bytes message W10 IoT Core LoRa! 251
 Transmit-Done
 Received 15 byte message HeLoRa World! 8
 Sending 22 bytes message W10 IoT Core LoRa! 250
 Transmit-Done
 Received 16 byte message HeLoRa World! 10
 Sending 22 bytes message W10 IoT Core LoRa! 249
 Transmit-Done
 Received 16 byte message HeLoRa World! 12
 Received 40 byte message ???LoRaIoT1#)#???c???h 55,t 24.9,s 3,v 4.01
 Sending 22 bytes message W10 IoT Core LoRa! 248
 Transmit-Done
 Received 16 byte message HeLoRa World! 14
 Sending 22 bytes message W10 IoT Core LoRa! 247
 Transmit-Done
 Received 16 byte message ???LoRaIoT1Maduino
 Sending 22 bytes message W10 IoT Core LoRa! 246
 Transmit-Done
 Received 16 byte message HeLoRa World! 18
 Sending 22 bytes message W10 IoT Core LoRa! 245
 Transmit-Done
 Received 16 byte message HeLoRa World! 20
 Sending 22 bytes message W10 IoT Core LoRa! 244
 Transmit-Done
 Received 16 byte message HeLoRa World! 22
 Sending 22 bytes message W10 IoT Core LoRa! 243
 Transmit-Done
 Received 16 byte message HeLoRa World! 24
 Sending 22 bytes message W10 IoT Core LoRa! 242
 Transmit-Done

The RegIrqFlags 01011000 indicates the RxDone, ValidHeader, and TxDone flags were set which was what I was expecting. Note the interference, the 46 byte packet

Next step is some refactoring to extract the register access code and merging with my Windows 10 IoT Core RMF9X library code base.

.Net Meadow RFM95/96/97/98 LoRa library Part7

Transmit Interrupt

Starting with the TransmitBasic sample application I modified the code so that a hardware interrupt (specified in RegDioMapping1) was generated on TxDone (FIFO Payload Transmission completed).

The application inserts a message into the RFM95 transmit FIFO every 10 seconds with confirmation of transmission displayed shortly afterwards

      public Rfm9XDevice(IIODevice device, ISpiBus spiBus, IPin chipSelectPin, IPin resetPin, IPin interruptPin)
      {
         // Chip select pin configuration
         ChipSelectGpioPin = device.CreateDigitalOutputPort(chipSelectPin, initialState: true);
         if (ChipSelectGpioPin == null)
         {
            Console.WriteLine("ChipSelectGpioPin == null");
         }

         // Factory reset pin configuration
         IDigitalOutputPort resetGpioPin = device.CreateDigitalOutputPort(resetPin);
         if (resetGpioPin == null)
         {
            Console.WriteLine("resetGpioPin == null");
         }
         resetGpioPin.State = false;
         Task.Delay(10);
         resetGpioPin.State = true;
         Task.Delay(10);

         // Interrupt pin for RX message & TX done notification 
         InterruptGpioPin = device.CreateDigitalInputPort(interruptPin, InterruptMode.EdgeRising);
         InterruptGpioPin.Changed += InterruptGpioPin_ValueChanged;

         Rfm9XLoraModem = new SpiPeripheral(spiBus, ChipSelectGpioPin);
         if (Rfm9XLoraModem == null)
         {
            Console.WriteLine("Rfm9XLoraModem == null");
         }
      }

      private void InterruptGpioPin_ValueChanged(object sender, DigitalInputPortEventArgs args)
      {
        byte irqFlags = this.RegisterReadByte(0x12); // RegIrqFlags
        this.RegisterWriteByte(0x12, 0xff);// Clear RegIrqFlags

        //Console.WriteLine(string.Format("RegIrqFlags:{0}", Convert.ToString(irqFlags, 2).PadLeft(8, '0')));
         if ((irqFlags & 0b00001000) == 0b00001000)  // TxDone
         {
            Console.WriteLine("Transmit-Done");
         }
      }
…
   public class MeadowApp : App<F7Micro, MeadowApp>
   {
      private Rfm9XDevice rfm9XDevice;

      public MeadowApp()
      {
         ISpiBus spiBus = Device.CreateSpiBus(500);
         if (spiBus == null)
         {
            Console.WriteLine("spiBus == null");
         }

         rfm9XDevice = new Rfm9XDevice(Device, spiBus, Device.Pins.D09, Device.Pins.D11, Device.Pins.D10);

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

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

         // More power PA Boost
         rfm9XDevice.RegisterWriteByte(0x09, 0b10000000); // RegPaConfig

         // Interrupt on TxDone
         rfm9XDevice.RegisterWriteByte(0x40, 0b01000000); // RegDioMapping1 0b00000000 DI0 TxDone

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

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

            string messageText = "Hello LoRa!";

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

            // Set the length of the message in the fifo
            rfm9XDevice.RegisterWriteByte(0x22, (byte)messageBytes.Length); // RegPayloadLength
            Console.WriteLine("Sending {0} bytes message {1}", messageBytes.Length, messageText);
            rfm9XDevice.RegisterWriteByte(0x01, 0b10000011); // RegOpMode 

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

The output in the debug window

'App.exe' (CLR v4.0.30319: DefaultDomain): Loaded 'C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll'. 
'App.exe' (CLR v4.0.30319: DefaultDomain): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.Meadow\TransmitInterrupt\bin\Debug\net472\App.exe'. Symbols loaded.
'App.exe' (CLR v4.0.30319: App.exe): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.Meadow\TransmitInterrupt\bin\Debug\net472\Meadow.dll'. 
The program '[11164] App.exe: Program Trace' has exited with code 0 (0x0).
The program '[11164] App.exe' has exited with code 0 (0x0).
.
.
DirectRegisterAccess = True
.
.
Sending 11 bytes message Hello LoRa!
Transmit-Done
Sending 11 bytes message Hello LoRa!
Transmit-Done
Sending 11 bytes message Hello LoRa!
Transmit-Done

On the Arduino test client the serial monitor displayed

13:02:09.098 -> Sending HeLoRa World! 4
13:02:19.130 -> Message: ⸮LoRaIoT1Maduino2at 79.7,ah 39,wsa 6,wsg 13,wd 28.13,r 0.00,
13:02:19.177 -> RSSI: -72
13:02:19.177 -> Snr: 9.25
13:02:19.177 -> 
13:02:19.431 -> Sending HeLoRa World! 6
13:02:29.994 -> Sending HeLoRa World! 8
13:02:32.000 -> Message: Hello LoRa!
13:02:32.000 -> RSSI: -46
13:02:32.047 -> Snr: 9.50
13:02:32.047 -> 
13:02:40.750 -> Sending HeLoRa World! 10
13:02:42.260 -> Message: Hello LoRa!
13:02:42.260 -> RSSI: -45
13:02:42.314 -> Snr: 9.50
13:02:42.314 -> 
13:02:51.286 -> Sending HeLoRa World! 12
13:02:52.541 -> Message: Hello LoRa!
13:02:52.541 -> RSSI: -45
13:02:52.541 -> Snr: 9.75
13:02:52.541 -> 
13:03:02.112 -> Sending HeLoRa World! 14
13:03:02.745 -> Message: Hello LoRa!
13:03:02.745 -> RSSI: -45
13:03:02.792 -> Snr: 9.50
13:03:02.792 -> 

Now that I’m confident my hardware is all working the next step will be building a full featured client based on my Windows 10 IoT Core library.

.Net Meadow RFM95/96/97/98 LoRa library Part6

Receive Interrupt

This proof of concept (PoC) was to confirm I could configure the Semtech 127X then handle the received messages using a Meadow event handler. The event handler code is based on the implementation in my Windows IoT 10 Core library.

I had added a few Console.WriteLine statements (Debug.Print currently doesn’t work Dec 2019) so I could see what was going on. But, using Console.WriteLine in the event handler caused me some problems which I had to debug. The irqFlags bit mask indicated there was a message in the FIFO but it wasn’t displayed and the interrupt mask wasn’t getting reset. As a temporary fix I refactored the code so the Console.WriteLine was the last statement in the EventHandler(which may cause other issues).

public Rfm9XDevice(IIODevice device, ISpiBus spiBus, IPin chipSelectPin, IPin resetPin, IPin interruptPin)
      {
         // Chip select pin configuration
         ChipSelectGpioPin = device.CreateDigitalOutputPort(chipSelectPin, initialState: true);
         if (ChipSelectGpioPin == null)
         {
            Console.WriteLine("ChipSelectGpioPin == null");
         }

         // Factory reset pin configuration
         IDigitalOutputPort resetGpioPin = device.CreateDigitalOutputPort(resetPin);
         if (resetGpioPin == null)
         {
            Console.WriteLine("resetGpioPin == null");
         }
         resetGpioPin.State = false;
         Task.Delay(10);
         resetGpioPin.State = true;
         Task.Delay(10);

         // Interrupt pin for RX message & TX done notification 
         InterruptGpioPin = device.CreateDigitalInputPort(interruptPin, InterruptMode.EdgeRising);
         InterruptGpioPin.Changed += InterruptGpioPin_ValueChanged;

         Rfm9XLoraModem = new SpiPeripheral(spiBus, ChipSelectGpioPin);
         if (Rfm9XLoraModem == null)
         {
            Console.WriteLine("Rfm9XLoraModem == null");
         }
      }
...
      private void InterruptGpioPin_ValueChanged(object sender, DigitalInputPortEventArgs args)
      {
         byte irqFlags = this.RegisterReadByte(0x12); // RegIrqFlags
         byte numberOfBytes = 0;
         string messageText = "";

         //Console.WriteLine(string.Format("RegIrqFlags:{0}", Convert.ToString(irqFlags, 2).PadLeft(8, '0')));
         if ((irqFlags & 0b01000000) == 0b01000000)
         {
            //Console.WriteLine("Receive-Message");
            byte currentFifoAddress = this.RegisterReadByte(0x10); // RegFifiRxCurrent
            this.RegisterWriteByte(0x0d, currentFifoAddress); // RegFifoAddrPtr

            numberOfBytes = this.RegisterReadByte(0x13); // RegRxNbBytes
            byte[] messageBytes = this.RegisterRead(0x00, numberOfBytes); // RegFifo
            messageText = UTF8Encoding.UTF8.GetString(messageBytes);
         }

         this.RegisterWriteByte(0x12, 0xff);// RegIrqFlags
         if (numberOfBytes > 0)
         {
            Console.WriteLine("Received {0} byte message {1}", numberOfBytes, messageText);
         }
      }
...
public class MeadowApp : App
{
private Rfm9XDevice rfm9XDevice;
  public MeadowApp()
  {
     ISpiBus spiBus = Device.CreateSpiBus(500);
     if (spiBus == null)
     {
        Console.WriteLine("spiBus == null");
     }

     rfm9XDevice = new Rfm9XDevice(Device, spiBus, Device.Pins.D09, Device.Pins.D11, Device.Pins.D10);

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

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

     rfm9XDevice.RegisterWriteByte(0x0F, 0x0); // RegFifoRxBaseAddress 

     rfm9XDevice.RegisterWriteByte(0x40, 0b00000000); // RegDioMapping1 0b00000000 DI0 RxReady & TxReady

     rfm9XDevice.RegisterWriteByte(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous

     rfm9XDevice.RegisterDump();

     Console.WriteLine("Receive-Wait");
     Task.Delay(-1).Wait();
  }
}

The output in the output debug window looked like this

'App.exe' (CLR v4.0.30319: DefaultDomain): Loaded 'C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll'. 
'App.exe' (CLR v4.0.30319: DefaultDomain): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.Meadow\ReceiveInterrupt\bin\Debug\net472\App.exe'. Symbols loaded.
'App.exe' (CLR v4.0.30319: App.exe): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.Meadow\ReceiveInterrupt\bin\Debug\net472\Meadow.dll'. 
The program '[40348] App.exe: Program Trace' has exited with code 0 (0x0).
The program '[40348] App.exe' has exited with code 0 (0x0).
.
.
DirectRegisterAccess = True
.
.
Register dump
Register 0x00 - Value 0X6c - Bits 01101100
…
Register 0x42 - Value 0X12 - Bits 00010010
Receive-Wait
Received 15 byte message HeLoRa World! 0
Received 59 byte message ???LoRaIoT1Maduino2at 57.2,ah 67,wsa 1,wsg 3,wd 56.63,r 0.00,
Received 15 byte message HeLoRa World! 2
Received 15 byte message HeLoRa World! 4
Received 15 byte message HeLoRa World! 6
Received 15 byte message HeLoRa World! 8
Received 16 byte message HeLoRa World! 10

Next steps will be wiring up the transmit done interrupt, then building a full featured client based on my Windows 10 IoT Core library.