TinyCLR OS V2 Seeed LoRa-E5 LoRaWAN library Part2

Nasty OTAA connect

After getting basic connectivity for my Seeedstudio LoRa-E5 Development Kit and Fezduino test rig working I wanted to see if I could get the device connected to The Things Industries(TTI) via the RAK7258 WisGate Edge Lite on the shelf in my office.

My Over the Air Activation (OTAA) implementation is very “nasty” as it is assumed that there are no timeouts or failures and it only sends one BCD message “48656c6c6f204c6f526157414e” which is “hello LoRaWAN”. The code just sequentially steps through the necessary commands (with a suitable delay after each is sent) to join the TTI network.

public class Program
{
#if TINYCLR_V2_FEZDUINO
   private static string SerialPortId = SC20100.UartPort.Uart5;
#endif
   private const string AppKey = "................................";

   //txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+ID=AppEui,{AppEui}\r\n"));
   //private const string AppEui = "................";

   //txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+ID=AppEui,\"{AppEui}\"\r\n"));
   private const string AppEui = ".. .. .. .. .. .. .. ..";

   private const byte messagePort = 1;

   //private const string payload = "48656c6c6f204c6f526157414e"; // Hello LoRaWAN
   private const string payload = "01020304"; // AQIDBA==
   //private const string payload = "04030201"; // BAMCAQ==

   public static void Main()
   {
      UartController serialDevice;
      int txByteCount;
      int rxByteCount;

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

      try
      {
         serialDevice = UartController.FromName(SerialPortId);

         serialDevice.SetActiveSettings(new UartSetting()
         {
            BaudRate = 9600,
            Parity = UartParity.None,
            StopBits = UartStopBitCount.One,
            Handshaking = UartHandshake.None,
            DataBits = 8
         });

         serialDevice.Enable();

         // Set the Region to AS923
         txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes("AT+DR=AS923\r\n"));
         Debug.WriteLine($"TX: DR {txByteCount} bytes");
         Thread.Sleep(500);

         // Read the response
         rxByteCount = serialDevice.BytesToRead;
         if (rxByteCount > 0)
         {
            byte[] rxBuffer = new byte[rxByteCount];
            serialDevice.Read(rxBuffer);
            Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
         }

         // Set the Join mode
         txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes("AT+MODE=LWOTAA\r\n"));
         Debug.WriteLine($"TX: MODE {txByteCount} bytes");
         Thread.Sleep(500);

         // Read the response
         rxByteCount = serialDevice.BytesToRead;
         if (rxByteCount > 0)
         {
            byte[] rxBuffer = new byte[rxByteCount];
            serialDevice.Read(rxBuffer);
            Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
         }

         // Set the appEUI
         txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+ID=AppEui,\"{AppEui}\"\r\n"));
         Debug.WriteLine($"TX: ID=AppEui {txByteCount} bytes");
         Thread.Sleep(500);

         // Read the response
         rxByteCount = serialDevice.BytesToRead;
         if (rxByteCount > 0)
         {
            byte[] rxBuffer = new byte[rxByteCount];
            serialDevice.Read(rxBuffer);
            Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
         }
            
         // Set the appKey
         txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+KEY=APPKEY,{AppKey}\r\n"));
         Debug.WriteLine($"TX: KEY=APPKEY {txByteCount} bytes");
         Thread.Sleep(500);

         // Read the response
         rxByteCount = serialDevice.BytesToRead;
         if (rxByteCount > 0)
         {
            byte[] rxBuffer = new byte[rxByteCount];
            serialDevice.Read(rxBuffer);
            Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
         }

         // Set the PORT
         txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+PORT={messagePort}\r\n"));
         Debug.WriteLine($"TX: PORT {txByteCount} bytes");
         Thread.Sleep(500);

         // Read the response
         rxByteCount = serialDevice.BytesToRead;
         if (rxByteCount > 0)
         {
            byte[] rxBuffer = new byte[rxByteCount];
            serialDevice.Read(rxBuffer);
            Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
         }

         // Join the network
         txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes("AT+JOIN\r\n"));
         Debug.WriteLine($"TX: JOIN {txByteCount} bytes");
         Thread.Sleep(10000);

         // Read the response
         rxByteCount = serialDevice.BytesToRead;
         if (rxByteCount > 0)
         {
            byte[] rxBuffer = new byte[rxByteCount];
            serialDevice.Read(rxBuffer);
            Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
         }

         while (true)
         {
            // Unconfirmed message
            txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+MSGHEX=\"{payload}\"\r\n"));
            Debug.WriteLine($"TX: MSGHEX {txByteCount} bytes");

            // Confirmed message
            //txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+CMSGHEX=\"{payload}\"\r\n"));
            //Debug.WriteLine($"TX: CMSGHEX {txByteCount} bytes");

            Thread.Sleep(10000);

            // Read the response
            rxByteCount = serialDevice.BytesToRead;
            if (rxByteCount > 0)
            {
               byte[] rxBuffer = new byte[rxByteCount];
               serialDevice.Read(rxBuffer);
               Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
            }

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

The code is not suitable for production but it confirmed my software and hardware configuration worked.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.IoT.SeeedE5.NetworkJoinOTAA starting
TX: DR 13 bytes
RX :+DR: AS923

TX: MODE 16 bytes
RX :+MODE: LWOTAA

TX: ID=AppEui 40 bytes
RX :+ID: AppEui, ..:..:.:.:.:.:.:.

TX: KEY=APPKEY 48 bytes
RX :+KEY: APPKEY ................................

TX: PORT 11 bytes
RX :+PORT: 1

TX: JOIN 9 bytes
RX :+JOIN: Start
+JOIN: NORMAL
+JOIN: Network joined
+JOIN: NetID 000013 DevAddr ..:..:..:..
+JOIN: Done

TX: MSGHEX 22 bytes
RX :+MSGHEX: Start
+MSGHEX: FPENDING
+MSGHEX: RXWIN1, RSSI -41, SNR 9.0
+MSGHEX: Done

TX: MSGHEX 22 bytes
RX :+MSGHEX: Start
+MSGHEX: Done

In the Visual Studio 2019 debug output I could see messages getting sent and then after a short delay they were visible in the TTI console.

Seeed E5 LoRaWAN dev Kit connecting in The Things Industries Device Live data tab

I had an issue with how the AppUI parameter was handled

   private const string AppKey = "................................";

   //txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+ID=AppEui,{AppEui}\r\n"));
   //private const string AppEui = "................";

   //txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"AT+ID=AppEui,\"{AppEui}\"\r\n"));
   private const string AppEui = ".. .. .. .. .. .. .. ..";


It appears that If the appkey (or other string parameter) has spaces it has to be enclosed in quotations.

TinyCLR OS V2 Seeed LoRa-E5 LoRaWAN library Part1

Basic connectivity

Today I have been working on a GHI Electronics TinyCLR V2  C# library for the Seeedstudio LoRa-E5 module using my Seeedstudio LoRa-E5 Development Kit.

My initial test rig is based on an Fezduino board with a Grove Base Shield V2.0 connected to
LoRa-E5 Development Kit by a Grove – Universal 4 Pin 20cm Unbuckled Cable(TX/RX reversed)

Fezduino Seeed LoRaE5 test rig

The code has compile time options for synchronous and asynchronous operation.

public class Program
{
   private static UartController serialDevice;
   private const string ATCommand = "at+ver\r\n";
#if TINYCLR_V2_FEZDUINO
   private static readonly string SerialPortId = SC20100.UartPort.Uart5;
#endif
#if TINYCLR_V2_SC20100DEV_MIKROBUS_1
   private const string SerialPortId = SC20100.UartPort.Usart2;
#endif
#if TINYCLR_V2_SC20100DEV_MIKROBUS_2
   private const string SerialPortId = SC20100.UartPort.Uart3;
#endif

   public static void Main()
   {
      Debug.WriteLine("devMobile.IoT.SeeedE5.ShieldSerial starting");

      try
      {
         serialDevice = UartController.FromName(SerialPortId);

         serialDevice.SetActiveSettings(new UartSetting()
         {
            BaudRate = 9600,
            Parity = UartParity.None,
            StopBits = UartStopBitCount.One,
            Handshaking = UartHandshake.None,
            DataBits = 8
         });

         serialDevice.Enable();

#if SERIAL_ASYNC_READ
         serialDevice.DataReceived += SerialDevice_DataReceived;
#endif

         while (true)
         {
            byte[] txBuffer = UTF8Encoding.UTF8.GetBytes(ATCommand);

            int txByteCount = serialDevice.Write(txBuffer);
            Debug.WriteLine($"TX: {txByteCount} bytes");

#if SERIAL_SYNC_READ
            while( serialDevice.BytesToWrite>0)
            {
               Debug.WriteLine($" BytesToWrite {serialDevice.BytesToWrite}");
               Thread.Sleep(100);
            }

            int rxByteCount = serialDevice.BytesToRead;
            if (rxByteCount>0)
            {
               byte[] rxBuffer = new byte[rxByteCount];

               serialDevice.Read(rxBuffer);

               Debug.WriteLine($"RX sync:{rxByteCount} bytes read");
               String response = UTF8Encoding.UTF8.GetString(rxBuffer);
               Debug.WriteLine($"RX sync:{response}");
            }
#endif

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

#if SERIAL_ASYNC_READ
   private static void SerialDevice_DataReceived(UartController sender, DataReceivedEventArgs e)
   {
      byte[] rxBuffer = new byte[e.Count];

      sender.Read(rxBuffer, 0, e.Count);

      Debug.WriteLine($"RX Async:{e.Count} bytes read");
      String response = UTF8Encoding.UTF8.GetString(rxBuffer);
      Debug.WriteLine($"RX Async:{response}");
   }
#endif
}

When I first ran the code no data was received so I doubled checked the serial connections and figured out I had to modify the cable and reverse the TX and RX pins.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.IoT.SeeedLoRaE5.ShieldSerial starting
TX: 8 bytes
RX Async:1 bytes read
RX Async:+
RX Async:8 bytes read
RX Async:VER: 4.0
RX Async:5 bytes read
RX Async:.11

TX: 8 bytes
RX Async:1 bytes read
RX Async:+
RX Async:9 bytes read
RX Async:VER: 4.0.
RX Async:4 bytes read
RX Async:11

RAK811/RAK4200 AS923 Join Channels

When running an application which used my TinyCLR V2 RAK811 Module library on a FezDuino with a modified RAK811 LPWAN Evaluation Board(EVB) most join attempts on my Things Industries(TTI) instance would fail. This was a bit odd as connecting to The Things Network(TTN) was pretty reliable.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.IoT.Rak811LoRaWanDeviceClient starting
12:00:12 Region AS923
12:00:12 ADR On
12:00:12 Unconfirmed
12:00:12 OTAA
12:00:13 Join start Timeout:30Sec
Join failed 26
The thread '<No Name>' (0x1) has exited with code 0 (0x0).
Done.

In TTI end device live data tab I could see the the joins attempts were failing with “Uplink channel Not found”

The Things Industries device live data tab “uplink channel not found” failures
The Things Industries device live data tab “uplink channel not found” detail

Initially I assumed this was an issue with my configuration of the RAKwireless RAK7258 gateway in my office that I was using for testing. After some discussions with a helpful TTI support person they suggested that I try disabling all bar the first two channels the RAK811 module was configured to use then see if worked.

I modified the intialise method of my TinyCLR V2 RAK811 Module library to disable all bar the first two channels

result = SendCommand("OK", "at+set_config=lora:ch_mask:2:0", CommandTimeoutDefault);
if (result != Result.Success)
{
#if DIAGNOSTICS
   Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} at+set_config=lora:ch_mask:2:0 {result}");
#endif
   return result;
}

result = SendCommand("OK", "at+set_config=lora:ch_mask:3:0", CommandTimeoutDefault);
if (result != Result.Success)
{
#if DIAGNOSTICS
   Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} at+set_config=lora:ch_mask:3:0 {result}");
#endif
   return result;
}

result = SendCommand("OK", "at+set_config=lora:ch_mask:4:0", CommandTimeoutDefault);
if (result != Result.Success)
{
#if DIAGNOSTICS
   Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} at+set_config=lora:ch_mask:4:0 {result}");
#endif
   return result;
}

result = SendCommand("OK", "at+set_config=lora:ch_mask:5:0", CommandTimeoutDefault);
if (result != Result.Success)
{
#if DIAGNOSTICS
    Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} at+set_config=lora:ch_mask:5:0 {result}");
#endif
   return result;
}

result = SendCommand("OK", "at+set_config=lora:ch_mask:6:0", CommandTimeoutDefault);
if (result != Result.Success)
{
#if DIAGNOSTICS
   Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} at+set_config=lora:ch_mask:6:0 {result}");
#endif
   return result;
}

result = SendCommand("OK", "at+set_config=lora:ch_mask:7:0", CommandTimeoutDefault);
if (result != Result.Success)
{
#if DIAGNOSTICS
   Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} at+set_config=lora:ch_mask:7:0 {result}");
#endif
   return result;
}

After modifying the code my Fezduino joined reliably.

The thread ” (0x2) has exited with code 0 (0x0).
devMobile.IoT.Rak811LoRaWanDeviceClient starting
12:00:12 Region AS923
12:00:12 ADR On
12:00:12 Unconfirmed
12:00:12 OTAA
12:00:13 Join start Timeout:30Sec
12:00:18 Join finish
Temperature : 19.9 °C
Pressure : 1014.0 HPa
Altitude : 143 meters
12:00:19 port:5 payload BCD:0073279C016700C8
12:00:44 Sleep
12:01:44 Wakeup
Temperature : 20.1 °C
Pressure : 1014.0 HPa
Altitude : 143 meters
12:01:44 port:5 payload BCD:0073279C016700C9
12:02:09 Sleep

The Things Industries device live data tab successful join.

After some further discussion with TTI support it looks like the RAK811 module doesn’t send join requests on the frequencies specified for the AS923 region in the LoRaWAN™1.1Regional Parameters.

LoRaWAN Regional Parameters AS923 Join-request frequencies

After confirming the join-request channel issue I went back to the RAKwireless forums with some new terms to search for and found that others were having a similar issue but with RAK4200 modules. My “best guess” is that the TTI implementation is more strict about join-request frequencies than the TTI

The Things Network V2 MQTT Client

Another option for I had been looking at for connecting an Azure IoT Hub and The Things Network(TTN) was a Message Queue Telemetry Transport(MQTT) integration.

To trial this approach I build a .Net Core console application which sent message to and received messages from an application running on a GHI Electronics TinyCLRV2 Fezduino with RakWireless Wisduino Evaluation Board(EVB).

The console application uses MQTTNet to connect to TTN. It subscribes to to the TTN application device uplink topic (did try subscribing to the uplink messages for all the devices in the application but this was to noisy), and the downlink message scheduled, sent and acknowledged topics. To send messages to the device I published them on the device downlink topic.

//string uplinktopic = $"{applicationId}/devices/+/up";
string uplinktopic = $"{applicationId}/devices/{deviceId}/up";
await mqttClient.SubscribeAsync(uplinktopic, MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce);

string downlinkAcktopic = $"{applicationId}/devices/{deviceId}/events/down/acks";
await mqttClient.SubscribeAsync(downlinkAcktopic, MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce);

string downlinkScheduledtopic = $"{applicationId}/devices/{deviceId}/events/down/scheduled";
await mqttClient.SubscribeAsync(downlinkScheduledtopic, MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce);

string downlinkSenttopic = $"{applicationId}/devices/{deviceId}/events/down/sent";
await mqttClient.SubscribeAsync(downlinkSenttopic, MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce);

string downlinktopic = $"{applicationId}/devices/{deviceId}/down";

I used the classes from one of my earlier blog posts to deserialise the uplink message payload so I could display a subset of the fields.

MQTTNet based .Net Core console client
Things Network Device Data view

In the TTN Device data tab I could see messages being sent, to and received from from the device.

Visual Studio 2019 Tiny CLR debugger Output

In the Visual Studio 2019 debugger output window I could see messages being sent and received by the Fezduino.

Malformed TTN downlink payload

I had some problems with the downlink messages silently failing as the TTN sample payload JSON was malformed and I had copied it without noticing.

I have a working TTN HTTP Integration (uplink messages only) but have been exploring alternatives using TTN MQTT and Azure IoT Hub AMQP clients.

The next step is to build an Azure IoT Hub client (using native AMQP) then join them together.

nRF24L01-TinyCLR V2 RC2 on Github

The source code of RC2 of my port GHI Electronics TinyCLR-0SV2RC1 nRF24L01 library is live on GitHub. The sample application now supports Fezduino (with embeddedcoolness.com or other Arduino shield), Fezportal and the SC2010 Dev board (with mikroe nrf24C Click, mikroe nRF24S Click or mikroenRF24T Click) .

Fezduino with Embedded Coolness shield
Fezportal with Mikroe nRF24 C Click
SC20100 Dev board

The application has gained four compile time configuration options

  • TINYCLR_V2_SC20100DEV_MIKROBUS_1
  • TINYCLR_V2_SC20100DEV_MIKROBUS_2
  • TINYCLR_V2_FEZDUINO
  • TINYCLR_V2_FEZPORTAL

These options configure the chip enable, chip selected and interrupt pins.

//---------------------------------------------------------------------------------
// Copyright (c) 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.
//
// Need one of TINYCLR_V2_SC20100DEV_MIKROBUS_1/TINYCLR_V2_SC20100DEV_MIKROBUS_2/TINYCLR_V2_FEZDUINO/TINYCLR_V2_FEZPORTAL defined
//---------------------------------------------------------------------------------
namespace devMobile.IoT.FieldGateway.TinyCLRV2nRF24Client
{
   using System;
   using System.Diagnostics;
   using System.Text;
   using System.Threading;

   using GHIElectronics.TinyCLR.Pins;

   using Radios.RF24;

   class Program
   {
      private const string BaseStationAddress = "Base1";
      private const string DeviceAddress = "Dev01";

      static void Main()
      {
         RF24 radio = new RF24();
         byte messageCount = System.Byte.MaxValue;

         try
         {
            radio.OnDataReceived += Radio_OnDataReceived;
            radio.OnTransmitFailed += Radio_OnTransmitFailed;
            radio.OnTransmitSuccess += Radio_OnTransmitSuccess;

#if TINYCLR_V2_SC20100DEV_MIKROBUS_1
            radio.Initialize(SC20100.SpiBus.Spi3, SC20100.GpioPin.PD4, SC20100.GpioPin.PD3, SC20100.GpioPin.PC5);
#endif
#if TINYCLR_V2_SC20100DEV_MIKROBUS_2
            radio.Initialize(SC20100.SpiBus.Spi3, SC20100.GpioPin.PD15, SC20100.GpioPin.PD14, SC20100.GpioPin.PA8);
#endif
#if TINYCLR_V2_FEZDUINO
            radio.Initialize(SC20100.SpiBus.Spi6, SC20100.GpioPin.PE11, SC20100.GpioPin.PC4, SC20100.GpioPin.PA1);
#endif
#if TINYCLR_V2_FEZPORTAL
            radio.Initialize(SC20100.SpiBus.Spi3, SC20100.GpioPin.PD4, SC20100.GpioPin.PC13, SC20100.GpioPin.PC2);
#endif
            radio.Address = Encoding.UTF8.GetBytes(DeviceAddress);

            radio.Channel = 15;
            radio.PowerLevel = PowerLevel.Minimum;
            radio.DataRate = DataRate.DR250Kbps;
            radio.IsEnabled = true;

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

            Debug.WriteLine($"Address: {Encoding.UTF8.GetString(radio.Address)}");
            Debug.WriteLine($"PowerLevel: {radio.PowerLevel}");
            Debug.WriteLine($"IsAutoAcknowledge: {radio.IsAutoAcknowledge}");
            Debug.WriteLine($"Channel: {radio.Channel}");
            Debug.WriteLine($"DataRate: {radio.DataRate}");
            Debug.WriteLine($"IsDynamicAcknowledge: {radio.IsDyanmicAcknowledge}");
            Debug.WriteLine($"IsDynamicPayload: {radio.IsDynamicPayload}");
            Debug.WriteLine($"IsEnabled: {radio.IsEnabled}");
            Debug.WriteLine($"Frequency: {radio.Frequency}");
            Debug.WriteLine($"IsInitialized: {radio.IsInitialized}");
            Debug.WriteLine($"IsPowered: {radio.IsPowered}");

            while (true)
            {
               string payload = $"hello {messageCount}";
               messageCount -= 1;

               Debug.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-TX {payload.Length} byte message {payload}");
               radio.SendTo(Encoding.UTF8.GetBytes(BaseStationAddress), Encoding.UTF8.GetBytes(payload));

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

      private static void Radio_OnDataReceived(byte[] data)
      {
         // display as hex
         Debug.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-RX Hex Length {data.Length} Payload {BitConverter.ToString(data)}");

         // Display as Unicode
         string unicodeText = Encoding.UTF8.GetString(data);
         Debug.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-RX Unicode Length {unicodeText.Length} Unicode text {unicodeText}");
      }

      private static void Radio_OnTransmitSuccess()
      {
         Debug.WriteLine($"{DateTime.UtcNow:HH:mm:ss}-TX Succeeded!");
      }

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

RFM9X.TinyCLR V2 RC2 on Github

The source code of RC2 of my GHI Electronics TinyCLR-0SV2RC1 RFM9X/SX127X library is live on GitHub. The sample application now supports Fezduino(dragino_LoRa shield for Arduino + others), Fezportal and the SC2010 Dev board (with CascoLogix LoRa Click) . I will add FezFeather and Fezstick support soon.

//---------------------------------------------------------------------------------
// Copyright (c) March/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.
//
// Need one of TINYCLR_V2_SC20100DEV_MIKROBUS_1/TINYCLR_V2_SC20100DEV_MIKROBUS_2/TINYCLR_V2_FEZDUINO/TINYCLR_V2_FEZPORTAL defined
//---------------------------------------------------------------------------------
namespace devMobile.IoT.Rfm9x.LoRaDeviceClient
{
	using System;
	using System.Diagnostics;
	using System.Text;
	using System.Threading;

	using GHIElectronics.TinyCLR.Pins;

	using devMobile.IoT.Rfm9x;

	class Program
   {
      static void Main()
      {
#if TINYCLR_V2_SC20100DEV_MIKROBUS_1 || TINYCLR_V2_SC20100DEV_MIKROBUS_2
			const string DeviceName = "SC20100DEVLoRa";
#endif
#if TINYCLR_V2_FEZDUINO
			const string DeviceName = "FezduinoLoRa";
#endif
#if TINYCLR_V2_FEZPORTAL
			const string DeviceName = "FezportalLoRa";
#endif
#if ADDRESSED_MESSAGES_PAYLOAD
			const string HostName = "LoRaIoT1";
#endif
			const double Frequency = 915000000.0;
			byte MessageCount = System.Byte.MaxValue;
#if TINYCLR_V2_SC20100DEV_MIKROBUS_1
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi3, SC20100.GpioPin.PD3, SC20100.GpioPin.PD4, SC20100.GpioPin.PC5);
#endif
#if TINYCLR_V2_SC20100DEV_MIKROBUS_2
			Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi3, SC20100.GpioPin.PD14, SC20100.GpioPin.PD15, SC20100.GpioPin.PA8);
#endif
#if TINYCLR_V2_FEZDUINO
			Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi6, SC20100.GpioPin.PB1, SC20100.GpioPin.PA15, SC20100.GpioPin.PA1);
#endif
#if TINYCLR_V2_FEZPORTAL
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(SC20100.SpiBus.Spi3, SC20100.GpioPin.PC13, SC20100.GpioPin.PD4,SC20100.GpioPin.PC2);
#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);
				Debug.WriteLine($"{DateTime.Now:HH:mm:ss}-TX {messageBytes.Length} byte message {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
			{
				string messageText = UTF8Encoding.UTF8.GetString(e.Data);

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

				Debug.WriteLine($@"{DateTime.Now:HH:mm:ss}-RX From {addressText} PacketSnr {e.PacketSnr} Packet RSSI {e.PacketRssi}dBm RSSI {e.Rssi}dBm = {e.Data.Length} byte message ""{messageText}""");
#else
				Debug.WriteLine($@"{DateTime.Now:HH:mm:ss}-RX PacketSnr {e.PacketSnr} Packet RSSI {e.PacketRssi}dBm RSSI {e.Rssi}dBm = {e.Data.Length} byte message ""{messageText}""");
#endif
			}
			catch (Exception ex)
			{
				Debug.WriteLine(ex.Message);
			}
		}

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

The library works but should be treated as late beta.

RAK811LoRaWAN.TinyCLR on Github

The source code of my TinyCLR V2 RAK811 Module library is now available on GitHub. My demonstration application uses a FezDuino and a modified RAK811 LPWAN Evaluation Board(EVB).

FezDuino with EVB plugged into Arduino headers

A sample application which shows how to connect using Over the Air Activation(OTAA) or Activation By Personalisation(ABP) then send and receive byte array/Binary Coded Decimal(BCD) messages .

//---------------------------------------------------------------------------------
// Copyright (c) July 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.
//
// must have one of 
//    TINYCLR_V2_FEZDUINO or
//    PAYLOAD_BCD or PAYLOAD_BYTES defined
//    OTAA or ABP
//
// For confirmed messages define CONFIRMED
//---------------------------------------------------------------------------------
namespace devMobile.IoT.Rak811LoRaWanDeviceClient
{
   using System;
   using System.Threading;
   using System.Diagnostics;

   using GHIElectronics.TinyCLR.Pins;
   using GHIElectronics.TinyCLR.Devices.Uart;

   using devMobile.IoT.LoRaWan;

   public class Program
   {
#if TINYCLR_V2_FEZDUINO
      private const string SerialPortId = SC20100.UartPort.Uart5;
#endif
#if OTAA
      private const string DevEui = "...";
      private const string AppEui = "...";
      private const string AppKey = "...";
#endif
#if ABP
      private const string DevAddress = "...";
      private const string NwksKey = "...";
      private const string AppsKey = "...";
#endif
      private const string Region = "AS923";
      private static readonly TimeSpan JoinTimeOut = new TimeSpan(0, 0, 10);
      private static readonly TimeSpan SendTimeout = new TimeSpan(0, 0, 10);
      private const byte MessagePort = 1;
#if PAYLOAD_BCD
      private const string PayloadBcd = "48656c6c6f204c6f526157414e"; // Hello LoRaWAN in BCD
#endif
#if PAYLOAD_BYTES
      private static readonly byte[] PayloadBytes = { 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x4c, 0x6f, 0x52, 0x61, 0x57, 0x41, 0x4e}; // Hello LoRaWAN in bytes
#endif

      public static void Main()
      {
         Result result;

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

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

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

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

               Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} ADR On");
               result = device.AdrOn();
               if (result != Result.Success)
               {
                  Debug.WriteLine($"ADR on failed {result}");
                  return;
               }

#if CONFIRMED
               Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Confirmed");
               result = device.Confirm(LoRaConfirmType.Confirmed);
               if (result != Result.Success)
               {
                  Debug.WriteLine($"Confirm on failed {result}");
                  return;
               }
#else
               Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Unconfirmed");
               result = device.Confirm(LoRaConfirmType.Unconfirmed);
               if (result != Result.Success)
               {
                  Debug.WriteLine($"Confirm off failed {result}");
                  return;
               }
#endif

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

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

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

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

                  // if we sleep module too soon response is missed
                  Thread.Sleep(5000);

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

                  Thread.Sleep(30000);

                  Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Wakeup");
                  result = device.Wakeup();
                  if (result != Result.Success)
                  {
                     Debug.WriteLine($"Wakeup failed {result}");
                     return;
                  }
               }
            }
         }
         catch (Exception ex)
         {
            Debug.WriteLine(ex.Message);
         }
      }

      static void OnMessageConfirmationHandler(int rssi, int snr)
      {
         Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Send Confirm RSSI:{rssi} SNR:{snr}");
      }

      static void OnReceiveMessageHandler(int port, int rssi, int snr, string payloadBcd)
      {
         byte[] payloadBytes = Rak811LoRaWanDevice.BcdToByes(payloadBcd);

         Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Receive Message RSSI:{rssi} SNR:{snr} Port:{port} Payload:{payloadBcd} PayLoadBytes:{BitConverter.ToString(payloadBytes)}");
      }
   }
}

I noticed (mid July 2020) that when changing from from ABP to OTAA and vice versa I needed to reset the the device “frame counters” in the Things Network console.

TinyCLR OS V2 RC1 RAK811 LoRaWAN library Part2

Nasty OTAA connect

After getting basic connectivity for my RAK811 LPWAN Evaluation Board(EVB) and Fezduino test rig working. I wanted to see if I could get the device connected to The Things Network(TTN) via the RAK7246G LPWAN Developer Gateway on my desk. I had got the EVB configuration sorted with a nanoFramework device so I was confident it should work.

EVB plugged into Fezduino

My Over the Air Activation (OTAA) implementation is very “nasty” I assumed that there would be no timeouts or failures and I only send one BCD message “48656c6c6f204c6f526157414e” which is “hello LoRaWAN”

I configured the RAK811 module for LoRaWAN

// Set the Working mode to LoRaWAN
txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes("at+set_config=lora:work_mode:0\r\n"));
Debug.WriteLine($"TX: work mode {txByteCount} bytes");
Thread.Sleep(500);

// Read the response
rxByteCount = serialDevice.BytesToRead;
if (rxByteCount > 0)
{
   byte[] rxBuffer = new byte[rxByteCount];
   serialDevice.Read(rxBuffer);
   Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
}
...

Then just sequentially step through the necessary configuration to join the TTN network with a suitable delay after each command is sent.

// Set the Region to AS923
txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes("at+set_config=lora:region:AS923\r\n"));
Debug.WriteLine($"TX: region {txByteCount} bytes");
Thread.Sleep(500);

// Read the response
rxByteCount = serialDevice.BytesToRead;
if (rxByteCount > 0)
{
   byte[] rxBuffer = new byte[rxByteCount];
   serialDevice.Read(rxBuffer);
   Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
}

// Set the JoinMode
txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes("at+set_config=lora:join_mode:0\r\n"));
Debug.WriteLine($"TX: join_mode {txByteCount} bytes");
Thread.Sleep(500);

// Read the response
rxByteCount = serialDevice.BytesToRead;
if (rxByteCount > 0)
{
   byte[] rxBuffer = new byte[rxByteCount];
   serialDevice.Read(rxBuffer);
   Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
}

// OTAA set the devEUI
txByteCount = serialDevice.Write(UTF8Encoding.UTF8.GetBytes($"at+set_config=lora:dev_eui:{DevEui}\r\n"));
Debug.WriteLine($"TX: dev_eui: {txByteCount} bytes");
Thread.Sleep(500);

// Read the response
rxByteCount = serialDevice.BytesToRead;
if (rxByteCount > 0)
{
   byte[] rxBuffer = new byte[rxByteCount];
   serialDevice.Read(rxBuffer);
   Debug.WriteLine($"RX :{UTF8Encoding.UTF8.GetString(rxBuffer)}");
}
...

The code is not suitable for production but it confirmed my software and hardware configuration worked.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.IoT.Rak811.NetworkJoinOTAA starting
TX: work mode 32 bytes
RX :UART1 work mode: RUI_UART_NORAMAL
Current work_mode:LoRaWAN, join_mode:OTAA, Class: A
Initialization OK 

TX: region 33 bytes
RX :OK 

TX: join_mode 32 bytes
RX :OK 

TX: dev_eui: 45 bytes
RX :OK 

TX: app_eui 45 bytes
RX :OK 

TX: app_key 61 bytes
RX :OK 

TX: confirm 30 bytes
RX :OK 

TX: join 9 bytes
RX :OK Join Success

TX: send 43 bytes
RX :OK 

TX: send 43 bytes
RX :OK 

In the Visual Studio 2019 debug output I could see messages getting sent and then after a short delay they were visible in the TTN console.

Successful OTAA Connect TTN logging

I had some issues with TimeSpan.ToString(…) throwing a CLR_E_UNSUPPORTED_INSTRUCTION exception which has been mentioned on the GHI Forums.

I had to modify my code to fix this issue

Debug.WriteLine($"{DateTime.UtcNow:hh:mm:ss} Join start Timeout:{JoinTimeOut:hh:mm:ss}");

// Became

Debug.WriteLine($" {DateTime.UtcNow:hh:mm:ss} Join Start Timeout {timeout.TotalSeconds} seconds");

I won’t bother with confirming any other functionality as I’m reasonably confident the nanoFramework library (which this code is based on) is working as expected.

TinyCLR OS V2 RC1 RAK811 LoRaWAN library Part1

Basic connectivity

Over the weekend I have been working on a GHI Electronics TinyCLR V2  C# library for my modified RAK811 LPWAN Evaluation Board(EVB) from RAK Wireless. My initial test rig is based on an Fezduino board which has Arduino Uno R3 format socket for the EVB.

Fezduino with RAK Wisnode shield

The code has compile time options for synchronous and asynchronous operation.

   public class Program
   {
      private static UartController serialDevice;
      private const string ATCommand = "at+version\r\n";
#if TINYCLR_V2_FEZDUINO
      private static string SerialPortId = SC20100.UartPort.Uart5;
#endif

      public static void Main()
      {
         Debug.WriteLine("devMobile.IoT.Rak811.ShieldSerial starting");

         try
         {
            serialDevice = UartController.FromName(SerialPortId);

            serialDevice.SetActiveSettings(new UartSetting()
            {
               BaudRate = 9600,
               Parity = UartParity.None,
               StopBits = UartStopBitCount.One,
               Handshaking = UartHandshake.None,
               DataBits = 8
            });

            serialDevice.Enable();

#if SERIAL_ASYNC_READ
            serialDevice.DataReceived += SerialDevice_DataReceived;
#endif

            while (true)
            {
               byte[] txBuffer = UTF8Encoding.UTF8.GetBytes(ATCommand);

               int txByteCount = serialDevice.Write(txBuffer);
               Debug.WriteLine($"TX: {txByteCount} bytes");

#if SERIAL_SYNC_READ
               while( serialDevice.BytesToWrite>0)
               {
                  Debug.WriteLine($" BytesToWrite {serialDevice.BytesToWrite}");
                  Thread.Sleep(100);
               }

               int rxByteCount = serialDevice.BytesToRead;
               if (rxByteCount>0)
               {
                  byte[] rxBuffer = new byte[rxByteCount];

                  serialDevice.Read(rxBuffer);

                  Debug.WriteLine($"RX sync:{rxByteCount} bytes read");
                  String response = UTF8Encoding.UTF8.GetString(rxBuffer);
                  Debug.WriteLine($"RX sync:{response}");
               }
#endif

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


#if SERIAL_ASYNC_READ
      private static void SerialDevice_DataReceived(UartController sender, DataReceivedEventArgs e)
      {
         byte[] rxBuffer = new byte[e.Count];

         serialDevice.Read(rxBuffer, 0, e.Count);

         Debug.WriteLine($"RX Async:{e.Count} bytes read");
         String response = UTF8Encoding.UTF8.GetString(rxBuffer);
         Debug.WriteLine($"RX Async:{response}");
      }
#endif
   }

When I first ran the code I noticed the serialDevice.Read timed out before any characters were received.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.IoT.Rak811.ShieldSerial starting
TX: 12 bytes
TX: 12 bytes
RX sync:19 bytes read
RX sync:OK V3.0.0.13.H.T3

TX: 12 bytes
RX sync:19 bytes read
RX sync:OK V3.0.0.13.H.T3

TX: 12 bytes
RX sync:19 bytes read
RX sync:OK V3.0.0.13.H.T3

I then added code to check the message had been sent and the code worked as expected. I now think, that rather than checking that the characters had been sent the short 100mSec delay was more important.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.IoT.Rak811.ShieldSerial starting
TX: 12 bytes
 BytesToWrite 10
RX sync:19 bytes read
RX sync:OK V3.0.0.13.H.T3

TX: 12 bytes
 BytesToWrite 10
RX sync:19 bytes read
RX sync:OK V3.0.0.13.H.T3

TX: 12 bytes
 BytesToWrite 10
RX sync:19 bytes read
RX sync:OK V3.0.0.13.H.T3

I then added code to receive data asynchronously and the response to the version request was received as expected.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.IoT.Rak811.ShieldSerial starting
TX: 12 bytes
RX Async:1 bytes read
RX Async:O
RX Async:8 bytes read
RX Async:K V3.0.0
RX Async:10 bytes read
RX Async:.13.H.T3

TX: 12 bytes
RX Async:1 bytes read
RX Async:O
RX Async:5 bytes read
RX Async:K V3.
RX Async:9 bytes read
RX Async:0.0.13.H.
RX Async:4 bytes read
RX Async:T3

RFM9X.TinyCLR V2 RC1 on Github

The source code of my GHI Electronics TinyCLR-0SV2RC1 RFM9X/SX127X library is live on GitHub. The test harness uses a Fezduino and a dragino technology LoRa shield for Arduino. I will add FezPortal, FezFeather and Fezstick support soon.

Fezduino with Dragino shield

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

//---------------------------------------------------------------------------------
// Copyright (c) March/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.
//
// Need one of TINYCLR_V2_SC20100DEV/TINYCLR_V2_FEZDUINO defined
//---------------------------------------------------------------------------------
namespace devMobile.IoT.Rfm9x.LoRaDeviceClient
{
	using System;
	using System.Diagnostics;
	using System.Text;
	using System.Threading;

	using GHIElectronics.TinyCLR.Pins;

	using devMobile.IoT.Rfm9x;

	class Program
   {
      static void Main()
      {
#if TINYCLR_V2_SC20100DEV
			const string DeviceName = "SC20100DEVLoRa";
#endif
#if TINYCLR_V2_FEZDUINO
			const string DeviceName = "FezduinoLoRa";
#endif
#if ADDRESSED_MESSAGES_PAYLOAD
			const string HostName = "LoRaIoT1";
#endif
			const double Frequency = 915000000.0;
			byte MessageCount = System.Byte.MaxValue;
#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
			
			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);
				Debug.WriteLine($"{DateTime.Now:HH:mm:ss}-TX {messageBytes.Length} byte message {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
			{
				string messageText = UTF8Encoding.UTF8.GetString(e.Data);

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

				Debug.WriteLine($@"{DateTime.Now:HH:mm:ss}-RX From {addressText} PacketSnr {e.PacketSnr} Packet RSSI {e.PacketRssi}dBm RSSI {e.Rssi}dBm = {e.Data.Length} byte message ""{messageText}""");
#else
				Debug.WriteLine($@"{DateTime.Now:HH:mm:ss}-RX PacketSnr {e.PacketSnr} Packet RSSI {e.PacketRssi}dBm RSSI {e.Rssi}dBm = {e.Data.Length} byte message ""{messageText}""");
#endif
			}
			catch (Exception ex)
			{
				Debug.WriteLine(ex.Message);
			}
		}

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

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