.NET Core SX127X library Part5

Receive and Transmit with Interrupts

After confirming my TransmitInterrupt and ReceiveInterupt test-rigs worked with an Arduino device running the SandeepMistry Arduino LoRa library LoRaSimpleNode example (LoRa.enableInvertIQ disabled) I merged them together.

For this client I’m using a Dragino Raspberry Pi HAT featuring GPS and LoRa® technology on my Raspberry PI.

Dragino pHat on my Raspberry 3 device
Arduino Monitor output running LoRaSimpleNode example code

The Dragino Raspberry Pi HAT featuring GPS and LoRa® technology hat has the same pin configuration as the M2M 1 Channel LoRaWAN Gateway Shield for Raspberry Pi so no code changes were required.

	class Program
	{
		static void Main(string[] args)
		{
			int messageCount = 1;
			// Uptronics has no reset pin uses CS0 or CS1
			//SX127XDevice sX127XDevice = new SX127XDevice(25, chipSelectLine: 0); 
			//SX127XDevice sX127XDevice = new SX127XDevice(25, chipSelectLine: 1); 

			// M2M device has reset pin uses non standard chip select 
			//SX127XDevice sX127XDevice = new SX127XDevice(4, chipSelectLine: 0, chipSelectLogicalPinNumber: 25, resetPin: 17);
			SX127XDevice sX127XDevice = new SX127XDevice(4, chipSelectLine: 1, chipSelectLogicalPinNumber: 25, resetPinNumber: 17);

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

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

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

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

			sX127XDevice.WriteByte(0x40, 0b00000000); // RegDioMapping1 0b00000000 DI0 RxReady & TxReady

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

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

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

				string messageText = "Hello LoRa from .NET Core! " + messageCount.ToString();
				messageCount += 1;

				// load the message into the fifo
				byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
				sX127XDevice.WriteBytes(0x00, messageBytes); // RegFifoAddrPtr 

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

				sX127XDevice.WriteByte(0x40, 0b01000000); // RegDioMapping1 0b00000000 DI0 RxReady & TxReady

				sX127XDevice.WriteByte(0x01, 0b10000011); // RegOpMode 

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

				Thread.Sleep(10000);
			}
		}
	}

For the SX127X to transmit and receive messages the device has to be put into sleep mode (RegOpMode), the frequency set to 915MHz(RegFrMsb, RegFrMid, RegFrLsb) and the receiver enabled(RxContinuous). In addition interrupts have to be enabled(RegDioMapping1) on message received(RxReady) and message sent(TxReady).

			sX127XDevice.WriteByte(0x40, 0b00000000); // RegDioMapping1 0b00000000 DI0 RxReady & TxReady

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

When running the applications sleeps the SX127X module(RegOpMode), writes the message payload to the buffer(RegFifoAddrPtr,RegPayloadLength) then turns on the transmitter(RegOpMode). When has message arrived or a message has been sent the DI0 pin is strobed, the type of interrupt is determined (RegIrqFlags) and processed accordingly. Once the interrupt has been processed the interrupt flags(RegIrqFlags) are cleared, the receiver re-enabled and the interrupt mappings reset(RegDioMapping1) reset.

Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/System.Memory.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Sending 28 bytes message Hello LoRa from .NET Core! 1
RegIrqFlags 00001000
Transmit-Done
RegIrqFlags 01010000
Receive-Message
Received 32 byte message HeLoRa World! I'm a Node! 880000
Sending 28 bytes message Hello LoRa from .NET Core! 2
RegIrqFlags 00001000
Transmit-Done
RegIrqFlags 01010000
Receive-Message
Received 32 byte message HeLoRa World! I'm a Node! 890000
Sending 28 bytes message Hello LoRa from .NET Core! 3
RegIrqFlags 00001000
Transmit-Done
RegIrqFlags 01010000
Receive-Message
Received 32 byte message HeLoRa World! I'm a Node! 900000
Sending 28 bytes message Hello LoRa from .NET Core! 4
RegIrqFlags 00001000
Transmit-Done
RegIrqFlags 01010000
Receive-Message
Received 32 byte message HeLoRa World! I'm a Node! 910000
Sending 28 bytes message Hello LoRa from .NET Core! 5
RegIrqFlags 00001000
Transmit-Done
RegIrqFlags 01010000
Receive-Message
Received 32 byte message HeLoRa World! I'm a Node! 920000

Summary

In this iteration I sent messages to and from my .Net Core 5 dotnet/iot powered Raspberry PI using a Dragino LoRa Shield 915MHz and a Seeeduino V4.2 client.

.NET Core SX127X library Part4

Receive Basic

Next step was proving I could receive a message sent by an Arduino device running the LoRaSimpleNode example from the SandeepMistry Arduino LoRa library. For some variety I’m using a Dragino Raspberry Pi HAT featuring GPS and LoRa® technology on my Raspberry PI.

Dragino pHat on my Raspberry 3 device
Arduino Monitor output running LoRaSimpleNode example code

The Dragino pHat has the same pin configuration as the M2M 1 Channel LoRaWAN Gateway Shield for Raspberry Pi so there weren’t any code changes.

class Program
{
	static void Main(string[] args)
	{
		// M2M device has reset pin uses non standard chip select 
		SX127XDevice sX127XDevice = new SX127XDevice(chipSelectLine: 1, chipSelectLogicalPinNumber: 25, resetPin: 17);

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

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

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

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

		while (true)
		{
			// Wait until a packet is received, no timeouts in PoC
			Console.WriteLine("Receive-Wait");
			byte IrqFlags = sX127XDevice.ReadByte(0x12); // RegIrqFlags
			while ((IrqFlags & 0b01000000) == 0)  // wait until RxDone cleared
			{
				Thread.Sleep(100);
				IrqFlags = sX127XDevice.ReadByte(0x12); // RegIrqFlags
				Debug.Write(".");
			}
			Console.WriteLine("");
			Console.WriteLine($"RegIrqFlags {Convert.ToString((byte)IrqFlags, 2).PadLeft(8, '0')}");
			Console.WriteLine("Receive-Message");
			byte currentFifoAddress = sX127XDevice.ReadByte(0x10); // RegFifiRxCurrent
			sX127XDevice.WriteByte(0x0d, currentFifoAddress); // RegFifoAddrPtr

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

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

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

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

			Console.WriteLine("Receive-Done");
		}
	}
}

There wasn’t much code to configure the SX127X to receive messages. The device has to be put into sleep mode (RegOpMode), the frequency set to 915MHz(RegFrMsb, RegFrMid, RegFrLsb) and receiver enabled(RxContinuous).

While running the applications polls (RegIrqFlags) until a message has arrived (RxDone). It then gets a pointer to the start of the message buffer (RegFifiRxCurrent, RegFifoAddrPtr), gets the message length, and then reads the message (RegPayloadLength, RegFifo) from the buffer. Finally the flags are reset ready for the next message(RegIrqFlags)

Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/System.Memory.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Sending 28 bytes message Hello LoRa from .NET Core! 1
RegIrqFlags 00001000
Transmit-Done
RegIrqFlags 01010000
Receive-Message
Received 31 byte message HeLoRa World! I'm a Node! 20000
Sending 28 bytes message Hello LoRa from .NET Core! 2
RegIrqFlags 00001000
Transmit-Done
RegIrqFlags 01010000
Receive-Message
Received 31 byte message HeLoRa World! I'm a Node! 30000
Sending 28 bytes message Hello LoRa from .NET Core! 3
RegIrqFlags 00001000
Transmit-Done
RegIrqFlags 01010000
Receive-Message
Received 31 byte message HeLoRa World! I'm a Node! 40000
Sending 28 bytes message Hello LoRa from .NET Core! 4
RegIrqFlags 00001000
Transmit-Done
RegIrqFlags 01010000
Receive-Message
Received 31 byte message HeLoRa World! I'm a Node! 50000
Sending 28 bytes message Hello LoRa from .NET Core! 5
RegIrqFlags 00001000
Transmit-Done
RegIrqFlags 01010000
Receive-Message
Received 31 byte message HeLoRa World! I'm a Node! 60000
Sending 28 bytes message Hello LoRa from .NET Core! 6
RegIrqFlags 00001000
Transmit-Done
RegIrqFlags 01010000
Receive-Message
Received 31 byte message HeLoRa World! I'm a Node! 70000
Sending 28 bytes message Hello LoRa from .NET Core! 7
RegIrqFlags 00001000
Transmit-Done
RegIrqFlags 01010000
Receive-Message
Received 31 byte message HeLoRa World! I'm a Node! 80000
Sending 28 bytes message Hello LoRa from .NET Core! 8
RegIrqFlags 00001000
Transmit-Done

Summary

In this iteration I sent a from my a Dragino LoRa Shield 915MHz on a Seeeduino V4.2 device to my .Net Core 5 dotnet/iot powered Raspberry PI.

TTI V3 Gateway provisioning Dragino LHT65 Uplink

This very long post is about how to connect a Dragino LHT65 Temperature and Humidity sensor to Azure IoT Central using my TTI/TTN V3Azure IoT Connector and the Digital Twin Definition Language (DTDL).

Dragino LHT65 temperature and Humidity sensor

The first step was to add an application(dragino-lht65) in my The Things Industries(TTI) tenant

TTI/TTN application for my Dragino LHT65 devices
Adding devMobile as a collaborator on the new application
TTI Application API Key configuration

The new Application API Key used by the MQTTnet managed client only needs to have write downlink and read uplink traffic enabled.

FTDI Adapter and modified LHT64 cable

So I could reliably connect to my LHT65 devices to configure them I modified a programming cable so I could use it with a spare FTDI adaptor without jumper wires. Todo this I used a small jewelers screwdriver to “pop” out the VCC cable and move the transmit data line.

After entering the device password and checking the firmware version I used the AT+CFG command to display the device settings

AT+CFG: Print all configurations

[334428]***** UpLinkCounter= 0 *****
[334430]TX on freq 923200000 Hz at DR 2
[334804]txDone
[339807]RX on freq 923200000 Hz at DR 2
[339868]rxTimeOut
[340807]RX on freq 923200000 Hz at DR 2
[340868]rxTimeOut

Correct Password

Stop Tx events,Please wait for all configurations to print
Printf all config...
AT+DEUI=a8 .. .. .. .. .. .. d6
AT+DADDR=01......D6

AT+APPKEY=9d .. .. .. .. .. .. .. .. .. .. .. .. .. .. 2e
AT+NWKSKEY=f6 .. .. .. .. .. .. .. .. .. .. .. .. .. .. 69
AT+APPSKEY=4c 35 .. .. .. .. .. .. .. .. .. .. .. .. .. 3d
AT+APPEUI=a0 .. .. .. .. .. .. 00
AT+ADR=1
AT+TXP=0
AT+DR=0
AT+DCS=0
AT+PNM=1
AT+RX2FQ=923200000
AT+RX2DR=2
AT+RX1DL=1000
AT+RX2DL=2000
AT+JN1DL=5000
AT+JN2DL=6000
AT+NJM=1
AT+NWKID=00 00 00 00
AT+FCU=0
AT+FCD=0
AT+CLASS=A
AT+NJS=0
AT+RECVB=0:
AT+RECV=0:
AT+VER=v1.7 AS923

AT+CFM=0
AT+CFS=0
AT+SNR=0
AT+RSSI=0
AT+TDC=1200000
AT+PORT=2
AT+PWORD=123456
AT+CHS=0
AT+DATE=21/3/26 07:49:15
AT+SLEEP=0
AT+EXT=4,2
AT+RTP=20
AT+BAT=3120
AT+WMOD=0
AT+ARTEMP=-40,125
AT+CITEMP=1
Start Tx events OK


[399287]***** UpLinkCounter= 0 *****

[399289]TX on freq 923400000 Hz at DR 2

[399663]txDone

[404666]RX on freq 923400000 Hz at DR 2

[404726]rxTimeOut

[405666]RX on freq 923200000 Hz at DR 2

[405726]rxTimeOut

I copied the AppEUI and DevEUI for use on the TI Dragino LHT65 Register end device form provided by the TTI/TTN.

TTYI/TTN Dragino LHT65 Register end device

The Dragino LHT65 uses the DeviceEUI as the DeviceID which meant I had todo more redaction in my TTI/TTN and Azure Application Insights screen captures. The rules around the re-use of EndDevice ID were a pain in the arse(PITA) in my development focused tenant.

Dragino LHT 65 Device uplink payload formatter

The connector supports both uplink and downlink messages with JSON encoded payloads. The Dragino LHT65 has a vendor supplied formatter which is automatically configured when an EndDevice is created. The EndDevice formatter configuration can also be overridden at the Application level in the app.settings.json file.

Device Live Data Uplink Data Payload

Once an EndDevice is configured in TTI/TTN I usually use the “Live data Uplink Payload” to work out the decoded payload JSON property names and data types.

LHT65 Uplink only Azure IoT Central Device Template
LHT65 Device Template View Identity

For Azure IoT Central “automagic” provisioning the DTDLModelId has to be copied from the Azure IoT Central Template into the TTI/TTN EndDevice or app.settings.json file application configuration.

LHT65 Device Template copy DTDL @ID
TTI EndDevice configuring the DTDLV2 @ID at the device level

Configuring the DTDLV2 @ID at the TTI application level in the app.settings.json file

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "Microsoft": "Debug",
      "Microsoft.Hosting.Lifetime": "Debug"
    },
    "ApplicationInsights": {
      "LogLevel": {
        "Default": "Debug"
      }
    }
  },

  "ProgramSettings": {
    "Applications": {
      "application1": {
        "AzureSettings": {
          "DeviceProvisioningServiceSettings": {
            "IdScope": "0ne...DD9",
            "GroupEnrollmentKey": "eFR...w=="
          }
        },
        "DTDLModelId": "dtmi:ttnv3connectorclient:FezduinoWisnodeV14x8;4",
        "MQTTAccessKey": "NNSXS.HCY...RYQ",
        "DeviceIntegrationDefault": false,
        "MethodSettings": {
          "Reboot": {
            "Port": 21,
            "Confirmed": true,
            "Priority": "normal",
            "Queue": "push"
          },
          "value_0": {
            "Port": 30,
            "Confirmed": true,
            "Priority": "normal",
            "Queue": "push"
          },
          "value_1": {
            "Port": 30,
            "Confirmed": true,
            "Priority": "normal",
            "Queue": "push"
          },
          "TemperatureOOBAlertMinimumAndMaximum": {
            "Port": 30,
            "Confirmed": true,
            "Priority": "normal",
            "Queue": "push"
          }
        }
      },
      "seeeduinolorawan": {
        "AzureSettings": {
          "DeviceProvisioningServiceSettings": {
            "IdScope": "0ne...DD9",
            "GroupEnrollmentKey": "AtN...g=="
          },
        },
        "DTDLModelId": "dtmi:ttnv3connectorclient:SeeeduinoLoRaWAN4cz;1",
        "MQTTAccessKey": "NNSXS.V44...42A",
        "DeviceIntegrationDefault": true,
        "DevicePageSize": 10
      },
      "dragino-lht65": {
        "AzureSettings": {
          "DeviceProvisioningServiceSettings": {
            "IdScope": "0ne...DD9",
            "GroupEnrollmentKey": "SLB...w=="
          }
        },
        "DTDLModelId": "dtmi:ttnv3connectorclient:DraginoLHT656w6;1",
        "MQTTAccessKey": "NNSXS.RIJ...NZQ",
        "DeviceIntegrationDefault": true,
        "DevicePageSize": 10
      }
    },
    "TheThingsIndustries": {
      "MqttServerName": "eu1.cloud.thethings.industries",
      "MqttClientId": "MQTTClient",
      "MqttAutoReconnectDelay": "00:00:05",
      "Tenant": "...-test",
      "ApiBaseUrl": "https://...-test.eu1.cloud.thethings.industries/api/v3",
      "ApiKey": "NNSXS.NR7...ZSA",
      "Collaborator": "devmobile",
      "DevicePageSize": 10,
      "DeviceIntegrationDefault": true
    }
  }
}

The Azure Device Provisioning Service(DPS) is configured at the TTI application level in the app.settings.json file. The IDScope and one of the Primary or Secondary Shared Access Signature(SAS) keys should be copied into DeviceProvisioningServiceSettings of an Application in the app.settings.json file. I usually set the “Automatically connect devices in this group” flag as part of the “automagic” provisioning process.

Azure IoT Central Group Enrollment Key
Then device templates need to be mapped to an Enrollment Group then Device Group.

For testing the connector application can be run locally with diagnostic information displayed in the application console window as it “automagically’ provisions devices and uploads telemetry data.

Connector application Diagnostics
Azure IoT Central Device list before my LHT65 device is “automagically” provisioned
Azure IoT Central Device list after my LHT65 device is “automagically” provisioned

One a device has been provisioned I check on the raw data display that all the fields I configured have been mapped correctly.

Azure IoT Central raw data display

I then created a dashboard to display the telemetry data from the LHT65 sensors.

Azure IoT Central dashboard displaying LHT65 temperature, humidity and battery voltage graphs.

The dashboard also has a few KPI displays which highlighted an issue which occurs a couple of times a month with the LHT65 onboard temperature sensor values (327.7°). I have connected Dragino technical support and have also been unable to find a way to remove the current an/or filter out future aberrant values.

Azure Application Insights logging

I also noticed that the formatting of the DeviceEUI values in the Application Insights logging was incorrect after trying to search for one of my Seeedstudio LoRaWAN device with its DeviceEUI.

TinyCLR OS V2 RC1 LoRa library Part2

Receive and Transmit

The first step was to confirm the transmission of messages with polled completion confirmation was working as expected.

   class Program
   {
      static void Main()
      {
#if TINYCLR_V2_SC20100DEV
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi3, SC20100.GpioPin.PA13, SC20100.GpioPin.PA14);
#endif
#if TINYCLR_V2_FEZDUINO
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi6, SC20100.GpioPin.PB1, SC20100.GpioPin.PA15);
#endif
         int SendCount = 0;

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

         rfm9XDevice.RegisterDump();

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

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

            string messageText = $"Hello LoRa {SendCount += 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

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

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

            Thread.Sleep(30000);
         }
      }
   }

The diagnostic output shows messages being sent and on another device I could see the messages arriving. I do wonder why the first message often takes so long to send?

Register dump
Register 0x00 - Value 0Xc3
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 0X80
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
Sending 13 bytes message Hello LoRa 1!
Send-wait
.
.
.
.
.
Send-Done
Sending 13 bytes message Hello LoRa 2!
Send-wait
Send-Done
Sending 13 bytes message Hello LoRa 3!
Send-wait
Send-Done
Sending 13 bytes message Hello LoRa 4!
Send-wait
Send-Done
Sending 13 bytes message Hello LoRa 5!
Send-wait
Send-Done
Sending 13 bytes message Hello LoRa 6!
Send-wait
Send-Done

The second step was to confirm the polled reception of messages was working as expected.

   class Program
   {
      static void Main()
      {
#if TINYCLR_V2_SC20100DEV
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi3, SC20100.GpioPin.PA13, SC20100.GpioPin.PA14);
#endif
#if TINYCLR_V2_FEZDUINO
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi6, SC20100.GpioPin.PB1, SC20100.GpioPin.PA15);
#endif


         // 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(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous

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

            byte numberOfBytes = rfm9XDevice.RegisterReadByte(0x13); // RegRxNbBytes

            byte[] messageBytes = rfm9XDevice.RegisterRead(0x00, numberOfBytes); // RegFifo

            rfm9XDevice.RegisterWriteByte(0x0d, 0);
            rfm9XDevice.RegisterWriteByte(0x12, 0b11111111); // RegIrqFlags clear all the bits

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

            Debug.WriteLine("Receive-Done");
         }
      }
   }

The diagnostic output shows messages being received from one of my other devices.

The thread ” (0x2) has exited with code 0 (0x0).
Receive-Wait

RegIrqFlags 0X50
Receive-Message
Received 23 byte message �LoRaIoT1N3WT 18.8,H 78
Receive-Done
Receive-Wait

RegIrqFlags 0X50
Receive-Message
Received 23 byte message �LoRaIoT1N3WT 18.8,H 78
Receive-Done
Receive-Wait

The next step was to confirm the interrupt driven reception of messages was working as expected.

   class Program
   {
      static void Main()
      {
#if TINYCLR_V2_SC20100DEV
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi3, SC20100.GpioPin.PA13, SC20100.GpioPin.PA14, SC20100.GpioPin.PE4);
#endif
#if TINYCLR_V2_FEZDUINO
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi6, SC20100.GpioPin.PB1, SC20100.GpioPin.PA15, SC20100.GpioPin.PA1);
#endif

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

         Debug.WriteLine("Receive-Wait");
         Thread.Sleep(Timeout.Infinite);
      }
   }

      private void InterruptGpioPin_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs e)
      {
         if (e.Edge != GpioPinEdge.RisingEdge)
         {
            return;
         }

         byte irqFlags = this.RegisterReadByte(0x12); // RegIrqFlags
         Debug.WriteLine($"RegIrqFlags 0X{irqFlags:x2}");
         if ((irqFlags & 0b01000000) == 0b01000000)  // RxDone 
         {
            Debug.WriteLine("Receive-Message");
            byte currentFifoAddress = this.RegisterReadByte(0x10); // RegFifiRxCurrent
            this.RegisterWriteByte(0x0d, currentFifoAddress); // RegFifoAddrPtr

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

            // Get number of bytes in the message
            byte[] messageBytes = this.RegisterRead(0x00, numberOfBytes);

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

         this.RegisterWriteByte(0x12, 0xff);// RegIrqFlags
      }

The diagnostic output shows messages being received from one of my other devices.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
Receive-Wait
RegIrqFlags 0X50
Receive-Message
Received 23 byte message  �LoRaIoT1N3WT 18.8,H 78
RegIrqFlags 0X50
Receive-Message
Received 23 byte message  �LoRaIoT1N3WT 18.7,H 79

The final step was to confirm the interrupt driven transmission of messages was working as expected.

   class Program
   {
      static void Main()
      {
#if TINYCLR_V2_SC20100DEV
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi3, SC20100.GpioPin.PA13, SC20100.GpioPin.PA14, SC20100.GpioPin.PE4);
#endif
#if TINYCLR_V2_FEZDUINO
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi6, SC20100.GpioPin.PB1, SC20100.GpioPin.PA15, SC20100.GpioPin.PA1); // Doesn't work
#endif
         int SendCount = 0;

         // 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 {SendCount += 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
            Debug.WriteLine($"Sending {messageBytes.Length} bytes message {messageText}");
            rfm9XDevice.RegisterWriteByte(0x01, 0b10000011); // RegOpMode 

            Thread.Sleep(10000);
         }
      }
   }


private void InterruptGpioPin_ValueChanged(GpioPin sender, GpioPinValueChangedEventArgs e)
      {
         if (e.Edge != GpioPinEdge.RisingEdge)
         {
            return;
         }

         byte irqFlags = this.RegisterReadByte(0x12); // RegIrqFlags
         Debug.WriteLine($"RegIrqFlags 0X{irqFlags:x2}");

         if ((irqFlags & 0b00001000) == 0b00001000)  // TxDone
         {
            Debug.WriteLine("Transmit-Done");
         }

         this.RegisterWriteByte(0x12, 0xff);// RegIrqFlags
      }

The diagnostic output shows messages being sent but after the first message (sometimes the second or third) there are no confirmations.

The thread ” (0x2) has exited with code 0 (0x0).
Sending 13 bytes message Hello LoRa 1!
RegIrqFlags 0X08
Transmit-Done
Sending 13 bytes message Hello LoRa 2!
Sending 13 bytes message Hello LoRa 3!
Sending 13 bytes message Hello LoRa 4!
Sending 13 bytes message Hello LoRa 5!
Sending 13 bytes message Hello LoRa 6!
Sending 13 bytes message Hello LoRa 7!
Sending 13 bytes message Hello LoRa 8!
Sending 13 bytes message Hello LoRa 9!
Sending 14 bytes message Hello LoRa 10!
Sending 14 bytes message Hello LoRa 11!
Sending 14 bytes message Hello LoRa 12!
Sending 14 bytes message Hello LoRa 13!
Sending 14 bytes message Hello LoRa 14!

It looks like something has been broken (possibly by RC1) in my implementation of interrupt driven transmission of messages.

TinyCLR OS V2 RC1 LoRa library Part1

The Basics

A week ago a selection of Single Board Computers(SBC) arrived from GHI Electronics. Previously I had been working with a SC20100 Dev board which has mikroBUS Click sockets which limited my peripheral options. There were several different device form factors in the package so I started with a Fezduino and Dragino LoRa shield for Arduino.

Fezduino with Dragino Shield

Need to be careful not to push the Dragino shield in too far as a couple of the pins (one is not connected and the other is IOREF) will contact the Micro SD card slot. (I have put a strip of Duct tape on the top of the Micro SD card socket)

Fezduino pin clearance

The first step was to get basic connectivity sorted. I opened the RFM9XLoRa-TinyCLR repository and modified the Serial Peripheral Interface(SPI) and chip select(CS) settings of the ShieldSPI project, then updated the NuGet packages (public feed rather than my local preview files).

Dragino LoRa Shield for Arduino pins

I have left the TinyCLR V1 configuration in for backward compatibility

#if TINYCLR_V1_FEZDUINO
            ChipSelectLine = FEZ.GpioPin.D10,
#endif
#if TINYCLR_V2_SC20100DEV
            ChipSelectLine = GHIElectronics.TinyCLR.Devices.Gpio.GpioController.GetDefault().OpenPin(SC20100.GpioPin.PA13),
#endif
#if TINYCLR_V2_FEZDUINO
            ChipSelectLine = GHIElectronics.TinyCLR.Devices.Gpio.GpioController.GetDefault().OpenPin(GHIElectronics.TinyCLR.Pins.SC20100.GpioPin.PB1),
#endif

When I ran the application in Visual Studio I could reliably read the RegVersion register.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
Value = 0x00-12
Value = 0x00-12
Value = 0x00-12
Value = 0x00-12
Value = 0x00-12
Value = 0x00-12
The program '[5] TinyCLR application: Managed' has exited with code 0 (0x0).

The next step was to modify the RegisterScan project to check I could read all the SX127X configuration registers.

  class Program
   {
      static void Main()
      {
#if TINYCLR_V2_SC20100DEV
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi3, SC20100.GpioPin.PA13);
#endif
#if TINYCLR_V2_FEZDUINO
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi6, SC20100.GpioPin.PB1);
#endif

         while (true)
         {
            for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
            {
               byte registerValue = rfm9XDevice.RegisterReadByte(registerIndex);

               Debug.WriteLine($"Register 0x{registerIndex:x2} - Value 0X{registerValue:x2}");
            }
            Debug.WriteLine("");

            Thread.Sleep(10000);
         }
      }
   }

When I ran the application in Visual Studio I could reliably read the registers 0x00 through 0x42.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
Register 0x00 - Value 0X00
Register 0x01 - Value 0X09
Register 0x02 - Value 0X1a
Register 0x03 - Value 0X0b
Register 0x04 - Value 0X00
Register 0x05 - Value 0X52
Register 0x06 - Value 0X6c
Register 0x07 - Value 0X80
Register 0x08 - Value 0X00
Register 0x09 - Value 0X4f
Register 0x0a - Value 0X09
Register 0x0b - Value 0X2b
Register 0x0c - Value 0X20
Register 0x0d - Value 0X08
Register 0x0e - Value 0X02
Register 0x0f - Value 0X0a
Register 0x10 - Value 0Xff
Register 0x11 - Value 0X71
Register 0x12 - Value 0X15
Register 0x13 - Value 0X0b
Register 0x14 - Value 0X28
Register 0x15 - Value 0X0c
Register 0x16 - Value 0X12
Register 0x17 - Value 0X47
Register 0x18 - Value 0X32
Register 0x19 - Value 0X3e
Register 0x1a - Value 0X00
Register 0x1b - Value 0X00
Register 0x1c - Value 0X00
Register 0x1d - Value 0X00
Register 0x1e - Value 0X00
Register 0x1f - Value 0X40
Register 0x20 - Value 0X00
Register 0x21 - Value 0X00
Register 0x22 - Value 0X00
Register 0x23 - Value 0X00
Register 0x24 - Value 0X05
Register 0x25 - Value 0X00
Register 0x26 - Value 0X03
Register 0x27 - Value 0X93
Register 0x28 - Value 0X55
Register 0x29 - Value 0X55
Register 0x2a - Value 0X55
Register 0x2b - Value 0X55
Register 0x2c - Value 0X55
Register 0x2d - Value 0X55
Register 0x2e - Value 0X55
Register 0x2f - Value 0X55
Register 0x30 - Value 0X90
Register 0x31 - Value 0X40
Register 0x32 - Value 0X40
Register 0x33 - Value 0X00
Register 0x34 - Value 0X00
Register 0x35 - Value 0X0f
Register 0x36 - Value 0X00
Register 0x37 - Value 0X00
Register 0x38 - Value 0X00
Register 0x39 - Value 0Xf5
Register 0x3a - Value 0X20
Register 0x3b - Value 0X82
Register 0x3c - Value 0Xfb
Register 0x3d - Value 0X02
Register 0x3e - Value 0X80
Register 0x3f - Value 0X40
Register 0x40 - Value 0X00
Register 0x41 - Value 0X00
Register 0x42 - Value 0X12

The next step was to modify the RegisterReadAndWrite project to check I could read and write the SX127X configuration registers.

     class Program
   {
      static void Main()
      {
#if TINYCLR_V2_SC20100DEV
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi3, SC20100.GpioPin.PA13, SC20100.GpioPin.PA14);
#endif
#if TINYCLR_V2_FEZDUINO
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi6, SC20100.GpioPin.PB1, SC20100.GpioPin.PA15);
#endif

         rfm9XDevice.RegisterDump();

         while (true)
         {
            Debug.WriteLine("Read RegOpMode (read byte)");
            Byte regOpMode1 = rfm9XDevice.RegisterReadByte(0x1);
            Debug.WriteLine($"RegOpMode 0x{regOpMode1:x2}");

            Debug.WriteLine("Set LoRa mode and sleep mode (write byte)");
            rfm9XDevice.RegisterWriteByte(0x01, 0b10000000);

            Debug.WriteLine("Read RegOpMode (read byte)");
            Byte regOpMode2 = rfm9XDevice.RegisterReadByte(0x1);
            Debug.WriteLine($"RegOpMode 0x{regOpMode2:x2}");

            Debug.WriteLine("Read the preamble (read word)");
            ushort preamble = rfm9XDevice.RegisterReadWord(0x20);
            Debug.WriteLine($"Preamble 0x{preamble:x2}");

            Debug.WriteLine("Set the preamble to 0x80 (write word)");
            rfm9XDevice.RegisterWriteWord(0x20, 0x80);

            Debug.WriteLine("Read the center frequency (read byte array)");
            byte[] frequencyReadBytes = rfm9XDevice.RegisterRead(0x06, 3);
            Debug.WriteLine($"Frequency Msb 0x{frequencyReadBytes[0]:x2} Mid 0x{frequencyReadBytes[1]:x2} Lsb 0x{frequencyReadBytes[2]:x2}");

            Debug.WriteLine("Set the center frequency to 915MHz (write byte array)");
            byte[] frequencyWriteBytes = { 0xE4, 0xC0, 0x00 };
            rfm9XDevice.RegisterWrite(0x06, frequencyWriteBytes);

            rfm9XDevice.RegisterDump();

            Thread.Sleep(30000);
         }
      }

When I ran the application in Visual Studio I could read and write register values.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
Register dump
Register 0x00 - Value 0X00
Register 0x01 - Value 0X09
Register 0x02 - Value 0X1a
Register 0x03 - Value 0X0b
Register 0x04 - Value 0X00
Register 0x05 - Value 0X52
Register 0x06 - Value 0X6c
Register 0x07 - Value 0X80
Register 0x08 - Value 0X00
Register 0x09 - Value 0X4f
Register 0x0a - Value 0X09
Register 0x0b - Value 0X2b
Register 0x0c - Value 0X20
Register 0x0d - Value 0X08
Register 0x0e - Value 0X02
Register 0x0f - Value 0X0a
Register 0x10 - Value 0Xff
Register 0x11 - Value 0X71
Register 0x12 - Value 0X15
Register 0x13 - Value 0X0b
Register 0x14 - Value 0X28
Register 0x15 - Value 0X0c
Register 0x16 - Value 0X12
Register 0x17 - Value 0X47
Register 0x18 - Value 0X32
Register 0x19 - Value 0X3e
Register 0x1a - Value 0X00
Register 0x1b - Value 0X00
Register 0x1c - Value 0X00
Register 0x1d - Value 0X00
Register 0x1e - Value 0X00
Register 0x1f - Value 0X40
Register 0x20 - Value 0X00
Register 0x21 - Value 0X00
Register 0x22 - Value 0X00
Register 0x23 - Value 0X00
Register 0x24 - Value 0X05
Register 0x25 - Value 0X00
Register 0x26 - Value 0X03
Register 0x27 - Value 0X93
Register 0x28 - Value 0X55
Register 0x29 - Value 0X55
Register 0x2a - Value 0X55
Register 0x2b - Value 0X55
Register 0x2c - Value 0X55
Register 0x2d - Value 0X55
Register 0x2e - Value 0X55
Register 0x2f - Value 0X55
Register 0x30 - Value 0X90
Register 0x31 - Value 0X40
Register 0x32 - Value 0X40
Register 0x33 - Value 0X00
Register 0x34 - Value 0X00
Register 0x35 - Value 0X0f
Register 0x36 - Value 0X00
Register 0x37 - Value 0X00
Register 0x38 - Value 0X00
Register 0x39 - Value 0Xf5
Register 0x3a - Value 0X20
Register 0x3b - Value 0X82
Register 0x3c - Value 0Xfa
Register 0x3d - Value 0X02
Register 0x3e - Value 0X80
Register 0x3f - Value 0X40
Register 0x40 - Value 0X00
Register 0x41 - Value 0X00
Register 0x42 - Value 0X12
Read RegOpMode (read byte)
RegOpMode 0x09
Set LoRa mode and sleep mode (write byte)
Read RegOpMode (read byte)
RegOpMode 0x80
Read the preamble (read word)
Preamble 0x08
Set the preamble to 0x80 (write word)
Read the center frequency (read byte array)
Frequency Msb 0x6c Mid 0x80 Lsb 0x00
Set the center frequency to 915MHz (write byte array)
Register dump
Register 0x00 - Value 0Xc3
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 0X4f
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 0X80
Register 0x21 - Value 0X00
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

At this point I was confident that I could hardware reset the shield and read/modify registers on the SX127X.

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.

RFM9X.NetNF on Github

The source code of my nanoFramework RFM9X/SX127X library is now available on GitHub. One test harness uses an STM32F429 Discovery and a dragino technology LoRa shield for Arduino with some jumper wires.

STM32F429 Discovery kit with Dragino Shield

The other uses Sparkfun LoRa Gateway 1 Channel ESP32 for a LoRaWAN.

A sample application which shows how to send/receive address/un-addressed payloads.

//---------------------------------------------------------------------------------
// 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 ST_STM32F429I_DISCOVERY       //nanoff --target ST_STM32F429I_DISCOVERY --update
//#define ESP32_WROOM_32_LORA_1_CHANNEL   //nanoff --target ESP32_WROOM_32 --serialport COM4 --update
namespace devMobile.IoT.Rfm9x.LoRaDeviceClient
{
	using System;
	using System.Text;
	using System.Threading;

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

	using devMobile.IoT.Rfm9x;

	class Program
	{
		const double Frequency = 915000000.0;
#if ST_STM32F429I_DISCOVERY
		private const string SpiBusId = "SPI5";
#endif
#if ESP32_WROOM_32_LORA_1_CHANNEL
		private const string SpiBusId = "SPI1";
#endif

		static void Main()
		{
			byte MessageCount = System.Byte.MaxValue;
#if ADDRESSED_MESSAGES_PAYLOAD
			const string HostName = "ESP32LoRa";
			const string DeviceName = "LoRaIoT1";
#else
			const string DeviceName = "ESP32LoRa";
#endif
#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 ST_STM32F429I_DISCOVERY
			Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SpiBusId, chipSelectPinNumber, resetPinNumber, interruptPinNumber);
#endif
			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(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);
				Console.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);

				Console.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
				Console.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)
			{
				Console.WriteLine(ex.Message);
			}
		}

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

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

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


The addressing support is pretty basic as my goal was a library that I could extend with optional functionality like tamper detection via signing and privacy via payload encryption, mesh network support etc.

The library works but should be treated as early beta.

nanoFramework LoRa library Part5

Receive Basic

This code implements the reception of messages builds on my transmit basic sample. I had to add a simple for loop to replace un-printable characters in the received message with spaces as nanoFramework UTF8Encoding.UTF8.GetString was throwing exceptions.

23:06:19.172 -> Sending HeLoRa World! 94
23:06:29.556 -> Sending HeLoRa World! 96
23:06:40.274 -> Sending HeLoRa World! 98
23:06:51.064 -> Sending HeLoRa World! 100
23:07:02.012 -> Sending HeLoRa World! 102
23:07:12.534 -> Sending HeLoRa World! 104
23:07:17.657 -> Message: ⸮LoRaIoT1Maduino2at 50.2,ah 90,wsa 4,wsg 11,wd 184.13,r 0.00,
23:07:17.725 -> Length: 61
23:07:17.725 -> FirstChar: 136
23:07:17.793 -> RSSI: -81
23:07:17.793 -> Snr: 9.50
23:07:17.793 -> 
23:07:23.216 -> Sending HeLoRa World! 106
23:07:34.228 -> Sending HeLoRa World! 108
23:07:44.907 -> Sending HeLoRa World! 110
23:07:55.930 -> Sending HeLoRa World! 112

For testing this code I used the same version of the LoRaSetSyncWord example as Transmit Basic

   class Program
   {
#if ST_STM32F429I_DISCOVERY
      private const string SpiBusId = "SPI5";
#endif
#if ESP32_WROOM_32_LORA_1_CHANNEL
      private const string SpiBusId = "SPI1";
#endif

      static void Main()
      {
#if ST_STM32F429I_DISCOVERY
         int chipSelectPinNumber = PinNumber('C', 2);
         int resetPinNumber = PinNumber('C', 3);
#endif
#if ESP32_WROOM_32_LORA_1_CHANNEL
         int chipSelectPinNumber = Gpio.IO16;
#endif

         try
         {
#if ESP32_WROOM_32_LORA_1_CHANNEL
            Configuration.SetPinFunction(Gpio.IO12, DeviceFunction.SPI1_MISO);
            Configuration.SetPinFunction(Gpio.IO13, DeviceFunction.SPI1_MOSI);
            Configuration.SetPinFunction(Gpio.IO14, DeviceFunction.SPI1_CLOCK);
            Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SpiBusId, chipSelectPinNumber);
#endif
#if ST_STM32F429I_DISCOVERY
            Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SpiBusId, chipSelectPinNumber, resetPinNumber);
#endif
             Thread.Sleep(500);

            // 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(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous

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

               byte numberOfBytes = rfm9XDevice.RegisterReadByte(0x13); // RegRxNbBytes

               byte[] messageBytes = rfm9XDevice.RegisterRead(0x00, numberOfBytes); // RegFifo

               rfm9XDevice.RegisterWriteByte(0x0d, 0);
               rfm9XDevice.RegisterWriteByte(0x12, 0b11111111); // RegIrqFlags clear all the bits

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

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

               Console.WriteLine("Receive-Done");
            }
         }
         catch (Exception ex)
         {
            Console.WriteLine(ex.Message);
         }
      }

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

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

The receive code works reliably but has no error detection or correction capability.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
Receive-Wait
................................................................
RegIrqFlags 0X50
Receive-Message
Received 17 byte message HeLoRa World! 114
Receive-Done
Receive-Wait
.......................................................................................................
RegIrqFlags 0X50
Receive-Message
Received 17 byte message HeLoRa World! 116
Receive-Done
Receive-Wait
.....................
RegIrqFlags 0X50
Receive-Message
Received 60 byte message  LoRaIoT1Maduino2at 50.2,ah 90,wsa 3,wsg 5,wd 178.50,r 0.00,
Receive-Done
Receive-Wait
.......................................................................................
RegIrqFlags 0X50
Receive-Message
Received 17 byte message HeLoRa World! 118
Receive-Done
Receive-Wait

I will look at implementing some sort of carrier-sense multiple access with collision avoidance solution to reduce the number of corrupted messages when two (or possibly more devices) transmit at the same time.

nanoFramework LoRa library Part4B

Transmit Basic Revisited

After finding some possible SPI library issues (April 2020) with my STM32F429 Discovery + Dragino LoRa shield for Arduino test rig I wanted to trial my code on another nanoFramework platform.

I had ordered a Sparkfun LoRa Gateway 1 Channel ESP32 for a LoRaWAN research project from a local supplier and an unexpected “bonus” was that the ESP32 WROOM platform is supported by the nanoFramework.

Sparkfun LoRa Gateway 1 Channel with wire antenna

I am using this in conjunction with my Armtronix IA005 SX1276 loRa node and my STM32F429 Discovery + Dragino LoRa shield for Arduino test rig.

STM32F429 Discovery+ Dragino LoRa shield with Armtronix device

The code now works on STM32F429 Discovery and ESP32 WROOM platforms. (manual update nanoFramework.Hardware.Esp32 NuGet reference required)

Sparkfun LoRa Gateway 1 Channel schematic

One disadvantage of the SparkFun device is that the reset pin on the SX127X doesn’t appear to be connected to the ESP32 so I can’t factory reset the device in code.

//#define ST_STM32F429I_DISCOVERY       //nanoff --target ST_STM32F429I_DISCOVERY --update
#define ESP32_WROOM_32_LORA_1_CHANNEL   //nanoff --target ESP32_WROOM_32 --serialport COM4 --update
namespace devMobile.IoT.Rfm9x.TransmitBasic
{
   using System;
   using System.Text;
   using System.Threading;

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

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

   public sealed class Rfm9XDevice
   {
      private SpiDevice rfm9XLoraModem;
      private const byte RegisterAddressReadMask = 0X7f;
      private const byte RegisterAddressWriteMask = 0x80;

      public Rfm9XDevice(string spiPort, int chipSelectPin, int resetPin)
      {
         var settings = new SpiConnectionSettings(chipSelectPin)
         {
            ClockFrequency = 1000000,
            //DataBitLength = 8,
            Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
            SharingMode = SpiSharingMode.Shared,
         };

         rfm9XLoraModem = SpiDevice.FromId(spiPort, settings);

         // Factory reset pin configuration
         GpioController gpioController = GpioController.GetDefault();
         GpioPin resetGpioPin = gpioController.OpenPin(resetPin);
         resetGpioPin.SetDriveMode(GpioPinDriveMode.Output);
         resetGpioPin.Write(GpioPinValue.Low);
         Thread.Sleep(10);
         resetGpioPin.Write(GpioPinValue.High);
         Thread.Sleep(10);
      }

      public Rfm9XDevice(string spiPort, int chipSelectPin)
      {
         var settings = new SpiConnectionSettings(chipSelectPin)
         {
            ClockFrequency = 1000000,
            Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
            SharingMode = SpiSharingMode.Shared,
         };

         rfm9XLoraModem = SpiDevice.FromId(spiPort, settings);
      }

      public Byte RegisterReadByte(byte registerAddress)
      {
         byte[] writeBuffer = new byte[] { registerAddress &= RegisterAddressReadMask, 0x0 };
         byte[] readBuffer = new byte[writeBuffer.Length];

         rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);

         return readBuffer[1];
      }

      public ushort RegisterReadWord(byte address)
      {
         byte[] writeBuffer = new byte[] { address &= RegisterAddressReadMask, 0x0, 0x0 };
         byte[] readBuffer = new byte[writeBuffer.Length];

         rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);

         return (ushort)(readBuffer[2] + (readBuffer[1] << 8));
      }

      public byte[] RegisterRead(byte address, int length)
      {
         byte[] writeBuffer = new byte[length + 1];
         byte[] readBuffer = new byte[writeBuffer.Length];
         byte[] repyBuffer = new byte[length];

         writeBuffer[0] = address &= RegisterAddressReadMask;

         rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);

         Array.Copy(readBuffer, 1, repyBuffer, 0, length);

         return repyBuffer;
      }

      public void RegisterWriteByte(byte address, byte value)
      {
         byte[] writeBuffer = new byte[] { address |= RegisterAddressWriteMask, value };
         byte[] readBuffer = new byte[writeBuffer.Length];

         rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);
      }

      public void RegisterWriteWord(byte address, ushort value)
      {
         byte[] valueBytes = BitConverter.GetBytes(value);
         byte[] writeBuffer = new byte[] { address |= RegisterAddressWriteMask, valueBytes[0], valueBytes[1] };
         byte[] readBuffer = new byte[writeBuffer.Length];

         rfm9XLoraModem.TransferFullDuplex(writeBuffer,readBuffer);
      }

      public void RegisterWrite(byte address, byte[] bytes)
      {
         byte[] writeBuffer = new byte[1 + bytes.Length];
         byte[] readBuffer = new byte[writeBuffer.Length];

         Array.Copy(bytes, 0, writeBuffer, 1, bytes.Length);
         writeBuffer[0] = address |= RegisterAddressWriteMask;

         rfm9XLoraModem.TransferFullDuplex(writeBuffer, readBuffer);
      }

      public void RegisterDump()
      {
         Console.WriteLine("Register dump");
         for (byte registerIndex = 0; registerIndex <= 0x42; registerIndex++)
         {
            byte registerValue = this.RegisterReadByte(registerIndex);

            Console.WriteLine($"Register 0x{registerIndex:x2} - Value 0X{registerValue:x2}");
         }
      }
   }

   class Program
   {
#if ST_STM32F429I_DISCOVERY
      private const string SpiBusId = "SPI5";
#endif
#if ESP32_WROOM_32_LORA_1_CHANNEL
      private const string SpiBusId = "SPI1";
#endif

      static void Main()
      {
         int SendCount = 0;
#if ST_STM32F429I_DISCOVERY
         int chipSelectPinNumber = PinNumber('C', 2);
         int resetPinNumber = PinNumber('C', 3);
#endif
#if ESP32_WROOM_32_LORA_1_CHANNEL
         int chipSelectPinNumber = Gpio.IO16;
#endif
         try
         {
#if ESP32_WROOM_32_LORA_1_CHANNEL
            Configuration.SetPinFunction(Gpio.IO12, DeviceFunction.SPI1_MISO);
            Configuration.SetPinFunction(Gpio.IO13, DeviceFunction.SPI1_MOSI);
            Configuration.SetPinFunction(Gpio.IO14, DeviceFunction.SPI1_CLOCK);
            Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SpiBusId, chipSelectPinNumber);
#endif
#if ST_STM32F429I_DISCOVERY
            Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SpiBusId, chipSelectPinNumber, resetPinNumber);
#endif
            Thread.Sleep(500);

            // Put device into LoRa + Standby mode
            rfm9XDevice.RegisterWriteByte(0x01, 0b10000001); // 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

            rfm9XDevice.RegisterDump();

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

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

               string messageText = $"Hello LoRa {SendCount += 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 {messageBytes.Length} bytes message {messageText}");
               /// Set the mode to LoRa + Transmit
               rfm9XDevice.RegisterWriteByte(0x01, 0b10000011); // RegOpMode 

               // Wait until send done, no timeouts in PoC
               Console.WriteLine("Send-wait");
               byte IrqFlags = rfm9XDevice.RegisterReadByte(0x12); // RegIrqFlags
               while ((IrqFlags & 0b00001000) == 0)  // wait until TxDone cleared
               {
                  Thread.Sleep(10);
                  IrqFlags = rfm9XDevice.RegisterReadByte(0x12); // RegIrqFlags
                  Console.WriteLine(".");
               }
               Console.WriteLine("");
               rfm9XDevice.RegisterWriteByte(0x12, 0b00001000); // clear TxDone bit
               Console.WriteLine("Send-Done");

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

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

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

When I initially ran the application in Visual Studio 2019 the text below was displayed in the output window.

Register dump
Register 0x00 - Value 0X00
Register 0x01 - Value 0X80
Register 0x02 - Value 0X1A
Register 0x03 - Value 0X0B
Register 0x04 - Value 0X00
…
Register 0x3E - Value 0X00
Register 0x3F - Value 0X00
Register 0x40 - Value 0X00
Register 0x41 - Value 0X00
Register 0x42 - Value 0X12
Sending 13 bytes message Hello LoRa 1!
Send-wait
.
.
.
.
.
Send-Done
Sending 13 bytes message Hello LoRa 2!
Send-wait
.
.
.
.
.
Send-Done

I could the see the messages arriving at the Armtronix device in the Arduino monitor.

18:21:46.299 -> Sending HeLoRa World! 188
18:21:48.700 -> Message: p8V⸮⸮⸮⸮⸮Kg
18:21:48.700 -> Length: 13
18:21:48.734 -> FirstChar: 112
18:21:48.734 -> RSSI: -70
18:21:48.734 -> Snr: 9.50
18:21:48.769 -> 
18:21:50.193 -> Message: Hello LoRa 10!
18:21:50.193 -> Length: 14
18:21:50.226 -> FirstChar: 72
18:21:50.226 -> RSSI: -49
18:21:50.226 -> Snr: 10.00
18:21:50.260 -> 
18:21:56.652 -> Sending HeLoRa World! 190
18:21:58.765 -> Message: Hello LoRa 2!
18:21:58.765 -> Length: 13
18:21:58.798 -> FirstChar: 72
18:21:58.798 -> RSSI: -71
18:21:58.798 -> Snr: 9.75
18:21:58.832 -> 
18:22:00.268 -> Message: Hello LoRa 11!
18:22:00.268 -> Length: 14
18:22:00.302 -> FirstChar: 72
18:22:00.302 -> RSSI: -49
18:22:00.302 -> Snr: 10.00
18:22:00.336 -> 

The first message was getting corrupted (only when running in the debugger) which after some trial and error I think was most probably due to my RegOpMode register mode configuration.

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

// Put device into LoRa + Standby mode
rfm9XDevice.RegisterWriteByte(0x01, 0b10000001); 

After a couple of years and half a dozen platform ports still finding bugs in my samples…

TinyCLR OS V2 LoRa library Part3

Transmit and Receive Basic

I had an Armtronix IA005 SX1276 loRa node sitting on my desk so used it running a modified version of the Arduino LoRa library LoRaSetSyncWord example to send messages to and receive messages from my SC20100 device.

Armtronix and SC20100 TinyCLR V2 testrig

The SC20100 transmit application configures the SX127X, sends a message, waits until transmission is completed, then repeats every 30 seconds.

   class Program
   {
      static void Main()
      {
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi3, SC20100.GpioPin.PA13, SC20100.GpioPin.PA14);

         int SendCount = 0;

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

         //rfm9XDevice.RegisterDump();

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

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

            string messageText = $"Hello LoRa {SendCount += 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

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

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

            Thread.Sleep(30000);
         }
      }
   }

When I ran the SC20100 application in Visual Studio

'GHIElectronics.TinyCLR.VisualStudio.ProjectSystem.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.TinyCLR\TransmitBasic\bin\Debug\pe\..\TransmitBasic.exe', Symbols loaded.
The thread '<No Name>' (0x2) has exited with code 0 (0x0).
Sending 13 bytes message Hello LoRa 1!
Send-wait
.
.
.
.
Send-Done
Sending 13 bytes message Hello LoRa 2!
Send-wait
.
.
.
.
Send-Done

I could the see the messages arriving at the Armtronix device in the Arduino monitor.

14:13:34.722 -> Message: Hello LoRa 1!
14:13:34.722 -> Length: 13
14:13:34.756 -> FirstChar: 72
14:13:34.756 -> RSSI: -48
14:13:34.756 -> Snr: 9.75
14:13:34.790 -> 
14:13:36.658 -> Sending HeLoRa World! 24
14:13:47.105 -> Sending HeLoRa World! 26
14:13:57.740 -> Sending HeLoRa World! 28
14:14:04.745 -> Message: Hello LoRa 2!
14:14:04.745 -> Length: 13
14:14:04.779 -> FirstChar: 72
14:14:04.779 -> RSSI: -49
14:14:04.779 -> Snr: 9.50
14:14:04.847 -> 

The SC20100 receive application configures the SX127X, polls a status register to looking to see if a message has arrived, displays it as text and then goes back to waiting.

   class Program
   {
      static void Main()
      {
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi3, SC20100.GpioPin.PA13, SC20100.GpioPin.PA14);

         // 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(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous

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

            byte numberOfBytes = rfm9XDevice.RegisterReadByte(0x13); // RegRxNbBytes

            byte[] messageBytes = rfm9XDevice.RegisterRead(0x00, numberOfBytes); // RegFifo

            rfm9XDevice.RegisterWriteByte(0x0d, 0);
            rfm9XDevice.RegisterWriteByte(0x12, 0b11111111); // RegIrqFlags clear all the bits

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

            Debug.WriteLine("Receive-Done");
         }
      }
   }

When I ran the SC20100 application in Visual Studio

'GHIElectronics.TinyCLR.VisualStudio.ProjectSystem.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.TinyCLR\ReceiveBasic\bin\Debug\pe\..\ReceiveBasic.exe', Symbols loaded.
The thread '<No Name>' (0x2) has exited with code 0 (0x0).
Receive-Wait

RegIrqFlags 0X50
Receive-Message
Received 16 byte message HeLoRa World! 74
Receive-Done
Receive-Wait

RegIrqFlags 0X50
Receive-Message
Received 59 byte message  �LoRaIoT1Maduino2at 64.6,ah 66,wsa 2,wsg 3,wd 37.13,r 0.00,
Receive-Done
Receive-Wait

RegIrqFlags 0X50
Receive-Message
Received 16 byte message HeLoRa World! 76
Receive-Done
Receive-Wait

RegIrqFlags 0X50
Receive-Message
Received 16 byte message HeLoRa World! 78
Receive-Done
Receive-Wait

I could the see the messages arriving at the Armtronix device in the Arduino monitor.

14:18:02.785 -> Sending HeLoRa World! 74
14:18:09.270 -> Message: ⸮LoRaIoT1Maduino2at 64.6,ah 66,wsa 2,wsg 3,wd 37.13,r 0.00,
14:18:09.339 -> Length: 59
14:18:09.339 -> FirstChar: 136
14:18:09.407 -> RSSI: -83
14:18:09.407 -> Snr: 9.75
14:18:09.407 -> 
14:18:13.249 -> Sending HeLoRa World! 76
14:18:23.416 -> Sending HeLoRa World! 78
14:18:33.582 -> Sending HeLoRa World! 80
14:18:43.883 -> Sending HeLoRa World! 82
14:18:54.136 -> Sending HeLoRa World! 84

I’ll merge the transmit and receive on interrupt samples in the next post as a final step before porting the core library modules.