TinyCLR OS LoRa library Part4

Transmit Basic

I had a couple of Armtronix IA005 SX1276 loRa nodes sitting on my desk from a recent post so I used one of them running a modified version of the Arduino LoRa library LoRaSetSyncWord example to receive messages from my device.

Armtronix and FEZT18-N TinyCLR testrig
/*
  LoRa Duplex communication with Sync Word
 
  Sends a message every half second, and polls continually
  for new incoming messages. Sets the LoRa radio's Sync Word.
 
  Spreading factor is basically the radio's network ID. Radios with different
  Sync Words will not receive each other's transmissions. This is one way you
  can filter out radios you want to ignore, without making an addressing scheme.
 
  See the Semtech datasheet, http://www.semtech.com/images/datasheet/sx1276.pdf
  for more on Sync Word.
 
  created 28 April 2017
  by Tom Igoe
*/
#include <stdlib.h>
#include <LoRa.h>
const int csPin = PA4;          // LoRa radio chip select
const int resetPin = PC13;       // LoRa radio reset
const int irqPin = PA11;         // change for your board; must be a hardware interrupt pin
 
byte msgCount = 0;            // count of outgoing messages
int interval = 2000;          // interval between sends
long lastSendTime = 0;        // time of last packet send
 
void setup() {
  Serial.begin(9600);                   // initialize serial
  while (!Serial);
 
  Serial.println("LoRa Duplex - Set sync word");
 
  // override the default CS, reset, and IRQ pins (optional)
  LoRa.setPins(csPin, resetPin, irqPin);// set CS, reset, IRQ pin
 
  if (!LoRa.begin(915E6)) {             // initialize ratio at 915 MHz
    Serial.println("LoRa init failed. Check your connections.");
    while (true);                       // if failed, do nothing
  }
 
  LoRa.setSyncWord(0x12);           // ranges from 0-0xFF, default 0x34, see API docs
 
  LoRa.dumpRegisters(Serial);
  Serial.println("LoRa init succeeded.");
}
 
void loop() {
  if (millis() - lastSendTime > interval) {
    String message = "HeLoRa World! ";   // send a message
    message += msgCount;
    sendMessage(message);
    Serial.println("Sending " + message);
    lastSendTime = millis();            // timestamp the message
    interval = random(1000) + 10000;    // 10-11 seconds
    msgCount++;
  }
 
  // parse for a packet, and call onReceive with the result:
  onReceive(LoRa.parsePacket());
}
 
void sendMessage(String outgoing) {
  LoRa.beginPacket();                   // start packet
  LoRa.print(outgoing);                 // add payload
  LoRa.endPacket();                     // finish packet and send it
  msgCount++;                           // increment message ID
}
 
void onReceive(int packetSize) {
  if (packetSize == 0) return;          // if there's no packet, return
 
  // read packet header bytes:
  String incoming = "";
 
  while (LoRa.available()) {
    incoming += (char)LoRa.read();
  }
 
  Serial.println("Message: " + incoming);
  Serial.println("RSSI: " + String(LoRa.packetRssi()));
  Serial.println("Snr: " + String(LoRa.packetSnr()));
  Serial.println();
}

The FEZT-18N application

   class Program
   {
      static void Main()
      {
         Rfm9XDevice rfm9XDevice = new Rfm9XDevice(FEZ.GpioPin.D10, FEZ.GpioPin.D9);
         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

         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 FEZ application in Visual Studio

'GHIElectronics.TinyCLR.VisualStudio.ProjectSystem.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.TinyCLR\TransmitBasic\bin\Debug\pe\..\GHIElectronics.TinyCLR.Native.dll'
'GHIElectronics.TinyCLR.VisualStudio.ProjectSystem.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.TinyCLR\TransmitBasic\bin\Debug\pe\..\GHIElectronics.TinyCLR.Devices.Gpio.dll'
'GHIElectronics.TinyCLR.VisualStudio.ProjectSystem.dll' (Managed): Loaded 'C:\Users\BrynLewis\source\repos\RFM9X.TinyCLR\TransmitBasic\bin\Debug\pe\..\GHIElectronics.TinyCLR.Devices.Spi.dll'
'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
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

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

19:22:28.445 -> Message: Hello LoRa 1!
19:22:28.445 -> RSSI: -36
19:22:28.479 -> Snr: 9.50
19:22:28.479 ->
19:22:29.362 -> Sending HeLoRa World! 48
19:22:40.390 -> Sending HeLoRa World! 50
19:22:50.654 -> Sending HeLoRa World! 52
19:22:58.484 -> Message: Hello LoRa 2!
19:22:58.484 -> RSSI: -36
19:22:58.553 -> Snr: 9.25
19:22:58.553 ->
19:23:01.254 -> Sending HeLoRa World! 54
19:23:11.873 -> Sending HeLoRa World! 56
19:23:22.103 -> Sending HeLoRa World! 58
19:23:28.558 -> Message: Hello LoRa 3!
19:23:28.558 -> RSSI: -31
19:23:28.592 -> Snr: 9.75
19:23:28.592 ->
19:23:32.307 -> Sending HeLoRa World! 60
19:23:42.929 -> Sending HeLoRa World! 62
19:23:53.392 -> Sending HeLoRa World! 64
19:23:58.622 -> Message: Hello LoRa 4!
19:23:58.622 -> RSSI: -36
19:23:58.655 -> Snr: 9.75
19:23:58.655 ->
19:24:03.629 -> Sending HeLoRa World! 66
19:24:14.299 -> Sending HeLoRa World! 68
19:24:24.989 -> Sending HeLoRa World! 70

This PoC code is getting a bit nasty with magic numbers and no error checking. The next step is getting a basic packet receive working…

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

Transmit Basic

I had a couple of Armtronix IA005 SX1276 loRa nodes sitting on my desk from a recent post so I used one of them running a modified version of the Arduino LoRa library LoRaSetSyncWord example to receive messages from my Meadow device.

/*
  LoRa Duplex communication with Sync Word
 
  Sends a message every half second, and polls continually
  for new incoming messages. Sets the LoRa radio's Sync Word.
 
  Spreading factor is basically the radio's network ID. Radios with different
  Sync Words will not receive each other's transmissions. This is one way you
  can filter out radios you want to ignore, without making an addressing scheme.
 
  See the Semtech datasheet, http://www.semtech.com/images/datasheet/sx1276.pdf
  for more on Sync Word.
 
  created 28 April 2017
  by Tom Igoe
*/
#include <stdlib.h>
#include <LoRa.h>
const int csPin = PA4;          // LoRa radio chip select
const int resetPin = PC13;       // LoRa radio reset
const int irqPin = PA11;         // change for your board; must be a hardware interrupt pin
 
byte msgCount = 0;            // count of outgoing messages
int interval = 2000;          // interval between sends
long lastSendTime = 0;        // time of last packet send
 
void setup() {
  Serial.begin(9600);                   // initialize serial
  while (!Serial);
 
  Serial.println("LoRa Duplex - Set sync word");
 
  // override the default CS, reset, and IRQ pins (optional)
  LoRa.setPins(csPin, resetPin, irqPin);// set CS, reset, IRQ pin
 
  if (!LoRa.begin(915E6)) {             // initialize ratio at 915 MHz
    Serial.println("LoRa init failed. Check your connections.");
    while (true);                       // if failed, do nothing
  }
 
  LoRa.setSyncWord(0x12);           // ranges from 0-0xFF, default 0x34, see API docs
 
  LoRa.dumpRegisters(Serial);
  Serial.println("LoRa init succeeded.");
}
 
void loop() {
  if (millis() - lastSendTime > interval) {
    String message = "HeLoRa World! ";   // send a message
    message += msgCount;
    sendMessage(message);
    Serial.println("Sending " + message);
    lastSendTime = millis();            // timestamp the message
    interval = random(1000) + 10000;    // 10-11 seconds
    msgCount++;
  }
 
  // parse for a packet, and call onReceive with the result:
  onReceive(LoRa.parsePacket());
}
 
void sendMessage(String outgoing) {
  LoRa.beginPacket();                   // start packet
  LoRa.print(outgoing);                 // add payload
  LoRa.endPacket();                     // finish packet and send it
  msgCount++;                           // increment message ID
}
 
void onReceive(int packetSize) {
  if (packetSize == 0) return;          // if there's no packet, return
 
  // read packet header bytes:
  String incoming = "";
 
  while (LoRa.available()) {
    incoming += (char)LoRa.read();
  }
 
  Serial.println("Message: " + incoming);
  Serial.println("RSSI: " + String(LoRa.packetRssi()));
  Serial.println("Snr: " + String(LoRa.packetSnr()));
  Serial.println();
}

The Meadow application

	public class MeadowApp : App<F7Micro, MeadowApp>
	{
		private Rfm9XDevice rfm9XDevice;

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

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

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

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

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

				string messageText = "Hello LoRa!";

				// load the message into the fifo
				byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
				foreach (byte b in messageBytes)
				{
					rfm9XDevice.RegisterWriteByte(0x0, b); // RegFifo
				}

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

				Console.WriteLine("Sending {0} bytes message {1}", messageBytes.Length, messageText);
				/// 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
				{
					Task.Delay(10).Wait();
					IrqFlags = rfm9XDevice.RegisterReadByte(0x12); // RegIrqFlags
					Console.Write(".");
				}
				Console.WriteLine("");
				rfm9XDevice.RegisterWriteByte(0x12, 0b00001000); // clear TxDone bit
				Console.WriteLine("Send-Done");

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

When I ran the meadow application after some messing around with the jumper wires.

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

I could the see the messages arriving at the Armtronix device in the Arduino monitor (the other messages in the monitor are my solar powered weather station and soil moisture monitoring node).

12:47:55.198 -> Sending HeLoRa World! 10
12:48:05.745 -> Sending HeLoRa World! 12
12:48:06.663 -> Message: ⸮LoRaIoT1Maduino2at 85.8,ah 19,wsa 5,wsg 8,wd 123.38,r 0.00,
12:48:06.730 -> RSSI: -71
12:48:06.730 -> Snr: 10.00
12:48:06.730 ->
12:48:08.770 -> Message: Hello LoRa!
12:48:08.770 -> RSSI: -47
12:48:08.804 -> Snr: 9.00
12:48:08.804 ->
12:48:16.555 -> Sending HeLoRa World! 14
12:48:26.847 -> Sending HeLoRa World! 16
12:48:37.154 -> Sending HeLoRa World! 18
12:48:39.469 -> Message: Hello LoRa!
12:48:39.469 -> RSSI: -46
12:48:39.536 -> Snr: 9.00
12:48:39.536 ->
12:48:47.311 -> Sending HeLoRa World! 20
12:48:58.094 -> Sending HeLoRa World! 22
12:49:07.748 -> Message: ⸮LoRaIoT1Maduino2at 86.0,ah 19,wsa 5,wsg 15,wd 155.63,r 0.00,
12:49:07.817 -> RSSI: -71
12:49:07.817 -> Snr: 9.50
12:49:07.817 ->
12:49:08.464 -> Sending HeLoRa World! 24
12:49:10.097 -> Message: Hello LoRa!
12:49:10.097 -> RSSI: -46
12:49:10.130 -> Snr: 9.75
12:49:10.130 ->
12:49:19.373 -> Sending HeLoRa World! 26
12:49:30.125 -> Sending HeLoRa World! 28
12:49:40.262 -> Sending HeLoRa World! 30
12:49:40.671 -> Message: Hello LoRa!
12:49:40.671 -> RSSI: -46
12:49:40.705 -> Snr: 9.25
12:49:40.705 ->
12:49:50.725 -> Sending HeLoRa World! 32
12:50:01.081 -> Sending HeLoRa World! 34
12:50:08.800 -> Message: ⸮LoRaIoT1Maduino2at 85.6,ah 19,wsa 5,wsg 11,wd 159.00,r 0.00,
12:50:08.868 -> RSSI: -72
12:50:08.868 -> Snr: 10.00
12:50:08.868 ->
12:50:11.219 -> Message: Hello LoRa!
12:50:11.219 -> RSSI: -46
12:50:11.252 -> Snr: 9.25
12:50:11.252 ->
12:50:11.526 -> Sending HeLoRa World! 36
12:50:21.731 -> Sending HeLoRa World! 38
12:50:32.696 -> Sending HeLoRa World! 40
12:50:41.741 -> Message: Hello LoRa!
12:50:41.741 -> RSSI: -46
12:50:41.775 -> Snr: 9.25
12:50:41.775 ->
12:50:43.685 -> Sending HeLoRa World! 42
12:50:54.566 -> Sending HeLoRa World! 44
12:51:05.604 -> Sending HeLoRa World! 46
12:51:09.852 -> Message: ⸮LoRaIoT1Maduino2at 85.3,ah 19,wsa 2,wsg 8,wd 150.75,r 0.00,
12:51:09.954 -> RSSI: -71
12:51:09.954 -> Snr: 9.50
12:51:09.954 ->
12:51:12.400 -> Message: Hello LoRa!
12:51:12.400 -> RSSI: -46
12:51:12.433 -> Snr: 9.00
12:51:12.433 ->
12:51:16.511 -> Sending HeLoRa World! 48
12:51:27.530 -> Sending HeLoRa World! 50
12:51:37.796 -> Sending HeLoRa World! 52
12:51:42.968 -> Message: Hello LoRa!
12:51:42.968 -> RSSI: -45
12:51:43.003 -> Snr: 9.25
12:51:43.003 ->
12:51:48.389 -> Sending HeLoRa World! 54
12:51:59.052 -> Sending HeLoRa World! 56
12:52:09.251 -> Sending HeLoRa World! 58
12:52:10.912 -> Message: ⸮LoRaIoT1Maduino2at 85.1,ah 19,wsa 2,wsg 6,wd 84.00,r 0.00,
12:52:11.013 -> RSSI: -70
12:52:11.013 -> Snr: 9.75
12:52:11.013 ->
12:52:13.546 -> Message: Hello LoRa!
12:52:13.546 -> RSSI: -46
12:52:13.581 -> Snr: 9.75
12:52:13.581 ->

This PoC code is getting a bit nasty with magic numbers and no error checking. The next step is getting a basic packet receive working…

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

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

To get started I used a Dragino LoRa shield for Arduino which looked compatible with my Meadow device.

Dragino shield schematic

The shield ships with the SPI lines configured for ICSP so the three jumpers diagonally across the shield from the antenna connector need to be swapped to the side closest to the edge of the shield.

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

   public class MeadowApp : App<F7Micro, MeadowApp>
   {
      const byte RegVersion = 0x42;
      ISpiBus spiBus;
      SpiPeripheral sx127xDevice;
      IDigitalOutputPort spiPeriphChipSelect;

      public MeadowApp()
      {
         ConfigureSpiPort();
         //ReadDeviceID();
         ReadDeviceIDDiy();
      }

      public void ConfigureSpiPort()
      {
         try
         {
            spiBus = Device.CreateSpiBus(500);
            if (spiBus == null)
            {
               Console.WriteLine("spiBus == null");
            }

            Console.WriteLine("Creating SPI NSS Port...");
            spiPeriphChipSelect = Device.CreateDigitalOutputPort(Device.Pins.D09);
            if (spiPeriphChipSelect == null)
            {
               Console.WriteLine("spiPeriphChipSelect == null");
            }
   
            Console.WriteLine("sx127xDevice Device...");
            sx127xDevice = new SpiPeripheral(spiBus, spiPeriphChipSelect);
            if (sx127xDevice == null)
            {
               Console.WriteLine("sx127xDevice == null");
            }

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

      
      public void ReadDeviceID()
      {
         Task.Delay(500).Wait();

         while (true)
         {
            try
            {
               Console.WriteLine("sx127xDevice.ReadRegister...1");

               byte registerValue = sx127xDevice.ReadRegister(RegVersion);

               Console.WriteLine("sx127xDevice.ReadRegister...2");

               Console.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", RegVersion, registerValue, Convert.ToString(registerValue, 2).PadLeft(8, '0'));
            }
            catch (Exception ex)
            {
               Console.WriteLine("ReadDeviceID " + ex.Message);
            }

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

      public void ReadDeviceIDDiy()
      {
         var txBuffer = new byte[2];
         var rxBuffer = new byte[2];

         Task.Delay(500).Wait();

         while (true)
         {
            try
            {
               Console.WriteLine("spiBus.ExchangeData...1");

               txBuffer[0] = RegVersion;

               spiBus.ExchangeData(spiPeriphChipSelect, ChipSelectMode.ActiveLow, txBuffer, rxBuffer, 2);

               Console.WriteLine("spiBus.ExchangeData...2");

               byte registerValue = rxBuffer[1];

               Console.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", RegVersion, registerValue, Convert.ToString(registerValue, 2).PadLeft(8, '0'));
            }
            catch (Exception ex)
            {
               Console.WriteLine("ReadDeviceIDDiy " + ex.Message);
            }

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

After some trial and error (using beta 3.6) I found that the ReadRegister method didn’t work as expected (possibly related to this issue) and I had to manually assemble the request to read the SX127X RegVersion register.

'App.exe' (CLR v4.0.30319: DefaultDomain): Loaded 'C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll'. 
'App.exe' (CLR v4.0.30319: DefaultDomain): Loaded 'C:\Users\BrynLewis\source\repos\RFX9X.Meadow\FeatherWingSPI\bin\Debug\net472\App.exe'. Symbols loaded.
'App.exe' (CLR v4.0.30319: App.exe): Loaded 'C:\Users\BrynLewis\source\repos\RFX9X.Meadow\FeatherWingSPI\bin\Debug\net472\Meadow.dll'. 
The program '[22324] App.exe: Program Trace' has exited with code 0 (0x0).
The program '[22324] App.exe' has exited with code 0 (0x0).
.
.
DirectRegisterAccess = True
==========================================================
Ignore the exceptions generated by the DateTime call here.
==========================================================
.
Creating SPI NSS Port...
sx127xDevice Device...
ConfigureSpiPort Done...
spiBus.ExchangeData...1
spiBus.ExchangeData...2
Register 0x42 - Value 0X12 - Bits 00010010
spiBus.ExchangeData...1
spiBus.ExchangeData...2
Register 0x42 - Value 0X12 - Bits 00010010
spiBus.ExchangeData...1
spiBus.ExchangeData...2
Register 0x42 - Value 0X12 - Bits 00010010
spiBus.ExchangeData...1
spiBus.ExchangeData...2

Maduino LoRa Air Temperature and Soil Moisture

This is a demo MakerFabs Maduino LoRa Radio 868MHz client (based on Maduino LoRa 868MHz example) that uploads telemetry data to my Windows 10 IoT Core on Raspberry PI AdaFruit.IO and Azure IoT Hub field gateways.

The code is available on github

Sample hardware
Azure IoT Central data visualisation

The Maduino device in the picture is a custom version with an onboard Microchip ATSHA204 crypto and authentication chip (currently only use for the unique 72 bit serial number) and a voltage divider connected to the analog pin A6 to monitor the battery voltage.

There are compile time options ATSHA204 & BATTERY_VOLTAGE_MONITOR which can be used to selectively enable this functionality.

I use the Arduino lowpower library to aggressively sleep the device between measurements

// Adjust the delay so period is close to desired sec as possible, first do 8sec chunks. 
  int delayCounter = SensorUploadDelay / 8 ;
  for( int i = 0 ; i < delayCounter ; i++ )
  {
     LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);  
  }
  
  // Then to 4 sec chunk
  delayCounter =  ( SensorUploadDelay % 8 ) / 4;
  for( int i = 0 ; i < delayCounter ; i++ )
  {
     LowPower.powerDown(SLEEP_4S, ADC_OFF, BOD_OFF);  
  }

  // Then to 2 sec chunk
  delayCounter =  ( SensorUploadDelay % 4 ) / 2 ;
  for( int i = 0 ; i < delayCounter ; i++ )
  {
     LowPower.powerDown(SLEEP_2S, ADC_OFF, BOD_OFF);  
  }

  // Then to 1 sec chunk
  delayCounter =  ( SensorUploadDelay % 2 ) ;
  for( int i = 0 ; i < delayCounter ; i++ )
  {
     LowPower.powerDown(SLEEP_1S, ADC_OFF, BOD_OFF);  
  }
}

I use a spare digital PIN for powering the soil moisture probe so it can be powered down when not in use. I have included a short delay after powering up the device to allow the reading to settle.

  // Turn on soil mosture sensor, take reading then turn off to save power
  digitalWrite(SoilMoistureSensorEnablePin, HIGH);
  delay(SoilMoistureSensorEnableDelay);
  int soilMoistureADCValue = analogRead(SoilMoistureSensorPin);
  digitalWrite(SoilMoistureSensorEnablePin, LOW);
  int soilMoisture = map(soilMoistureADCValue,SoilMoistureSensorMinimum,SoilMoistureSensorMaximum, SoilMoistureValueMinimum, SoilMoistureValueMaximum); 
  PayloadAdd( "s", soilMoisture, false);

Bill of materials (Prices Nov 2019)

  • Maduino LoRa Radion (868MHz) 18.90
  • SHT20 I2C Temperature & Humidity Sensor (Waterproof Probe) USD22.50
  • Pinotech SoilWatch 10 – Soil moisture sensor USD23
  • Elecrow 1 Watt solar panel with wires USD3.80
  • 500 mAh LI-Ion battery

The software could easily be modified to support additional sensors.

RFM69 hat library Part12A

Enums and Masks

Based on the approach I used in my RFM9X library this refactor adds enumerations and constants for initialising and then accessing the registers of the RFM69CW/RFM69HCW module (based on the Semtech SX1231/SX1231H) .

Adafruit RFM69 Radio Bonnet

There is now a even less code in the Run method in the startup.cs file and the code for configuring the RFM69 is more obvious

public sealed class StartupTask : IBackgroundTask
{
	private const int ResetPin = 25;
	private const int InterruptPin = 22;
	private Rfm69HcwDevice rfm69Device = new Rfm69HcwDevice(ChipSelectPin.CS1, ResetPin, InterruptPin);

	public void Run(IBackgroundTaskInstance taskInstance)
	{
		byte[] syncValues ={0xAA, 0x2D, 0xD4};

		rfm69Device.Initialise(Rfm69HcwDevice.RegOpModeMode.StandBy,
			bitRate: Rfm69HcwDevice.BitRate.bps4K8,
			frequency: 915000000.0, frequencyDeviation: 0X023d,
			dccFrequency: 0x1,rxBwMant: Rfm69HcwDevice.RxBwMant.RxBwMant20, RxBwExp:0x2,
			preambleSize: 16,
			syncSize: 3,
			syncValues: syncValues,
			packetFormat: Rfm69HcwDevice.RegPacketConfig1PacketFormat.VariableLength,
			packetCrc:true
		);

	// RegDioMapping1
	rfm69Device.RegisterManager.WriteByte(0x26, 0x01);

	rfm69Device.RegisterDump();

	while (true)
	{
		byte[] messageBuffer = UTF8Encoding.UTF8.GetBytes("hello world " + DateTime.Now.ToLongTimeString());

		rfm69Device.SendMessage(messageBuffer);

		Debug.WriteLine("{0:HH:mm:ss.fff} Send-Done", DateTime.Now);

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

The Rasmitic library modifies a number of the default settings (e.g. RegRxBw register) so I had to reverse engineer the values. I also added SendMessage methods for both addressed and un-addressed messages.

Register dump
Register 0x01 - Value 0X04 - Bits 00000100
Register 0x02 - Value 0X00 - Bits 00000000
Register 0x03 - Value 0X1a - Bits 00011010
…
Register 0x4b - Value 0X00 - Bits 00000000
Register 0x4c - Value 0X00 - Bits 00000000
Register 0x4d - Value 0X00 - Bits 00000000
16:52:38.337 Send-Done
16:52:38.456 RegIrqFlags 00001000
16:52:38.472 Transmit-Done
The thread 0xfe4 has exited with code 0 (0x0).
The thread 0x100 has exited with code 0 (0x0).
16:52:43.391 Send-Done
16:52:43.465 RegIrqFlags 00001000
16:52:43.480 Transmit-Done
The thread 0xb94 has exited with code 0 (0x0).
16:52:48.475 Send-Done
16:52:48.550 RegIrqFlags 00001000
16:52:48.563 Transmit-Done
16:52:53.448 RegIrqFlags 01000110
16:52:53 Received 13 byte message Hello world:0
The thread 0x2b4 has exited with code 0 (0x0).
16:52:53.559 Send-Done
16:52:53.633 RegIrqFlags 00001000
16:52:53.648 Transmit-Done
16:52:54.577 RegIrqFlags 01000110
16:52:54 Received 13 byte message Hello world:1
16:52:55.706 RegIrqFlags 01000110
16:52:55 Received 13 byte message Hello world:2
16:52:56.836 RegIrqFlags 01000110
16:52:56 Received 13 byte message Hello world:3
16:52:57.965 RegIrqFlags 01000110
16:52:57 Received 13 byte message Hello world:4
The thread 0x354 has exited with code 0 (0x0).
16:52:58.634 Send-Done
16:52:58.709 RegIrqFlags 00001000
16:52:58.724 Transmit-Done
16:52:59.095 RegIrqFlags 01000110
16:52:59 Received 13 byte message Hello world:5
The program '[3736] backgroundTaskHost.exe' has exited with code -1 

The Arduino code works though I need modify it so I can do more testing of the initialise method parameter options.

16:41:03.619 -> RX start
16:41:03.619 -> 0x0: 0x0
16:41:03.654 -> 0x1: 0x10
16:41:03.654 -> 0x2: 0x0
…
16:41:04.310 -> 0x3B: 0x0
16:41:04.310 -> 0x3C: 0x1
16:41:04.344 -> 0x3D: 0x0
16:41:07.228 -> MessageIn:hello world 4:41:07 PM
16:41:12.322 -> MessageIn:hello world 4:41:12 PM
16:41:17.395 -> MessageIn:hello world 4:41:17 PM
16:41:22.448 -> MessageIn:hello world 4:41:22 PM
16:41:27.533 -> MessageIn:hello world 4:41:27 PM
16:41:32.609 -> MessageIn:hello world 4:41:32 PM
16:41:37.673 -> MessageIn:hello world 4:41:37 PM

The Initialise method has a large number of parameters but as most of these have a reasonable default I’m not to concerned.

public void Initialise(RegOpModeMode modeAfterInitialise,
			BitRate bitRate = BitRateDefault,
			ushort frequencyDeviation = frequencyDeviationDefault,
			double frequency = FrequencyDefault,
			ListenModeIdleResolution listenModeIdleResolution = ListenModeIdleResolutionDefault, ListenModeRXTime listenModeRXTime = ListenModeRXTimeDefault, ListenModeCrieria listenModeCrieria = ListenModeCrieriaDefault, ListenModeEnd listenModeEnd = ListenModeEndDefault,
			byte listenCoefficientIdle = ListenCoefficientIdleDefault,
			byte listenCoefficientReceive = ListenCoefficientReceiveDefault,
			bool pa0On = pa0OnDefault, bool pa1On = pa1OnDefaut, bool pa2On = pa2OnDefault, byte outputpower = OutputpowerDefault,
			PaRamp paRamp = PaRampDefault,
			bool ocpOn = OcpOnDefault, byte ocpTrim = OcpTrimDefault,
			LnaZin lnaZin = LnaZinDefault, LnaCurrentGain lnaCurrentGain = LnaCurrentGainDefault, LnaGainSelect lnaGainSelect = LnaGainSelectDefault,
			byte dccFrequency = DccFrequencyDefault, RxBwMant rxBwMant = RxBwMantDefault, byte RxBwExp = RxBwExpDefault,
			byte dccFreqAfc = DccFreqAfcDefault, byte rxBwMantAfc = RxBwMantAfcDefault, byte bxBwExpAfc = RxBwExpAfcDefault,
			ushort preambleSize = PreambleSizeDefault,
			bool syncOn = SyncOnDefault, SyncFifoFileCondition syncFifoFileCondition = SyncFifoFileConditionDefault, byte syncSize = SyncSizeDefault, byte syncTolerance = SyncToleranceDefault, byte[] syncValues = null,
			RegPacketConfig1PacketFormat packetFormat = RegPacketConfig1PacketFormat.FixedLength,
			RegPacketConfig1DcFree packetDcFree = RegPacketConfig1DcFreeDefault,
			bool packetCrc = PacketCrcOnDefault,
			bool packetCrcAutoClearOff = PacketCrcAutoClearOffDefault,
			RegPacketConfig1CrcAddressFiltering packetAddressFiltering = PacketAddressFilteringDefault,
			byte payloadLength = PayloadLengthDefault,
			byte addressNode = NodeAddressDefault, byte addressbroadcast = BroadcastAddressDefault
			)
		{
			RegOpModeModeCurrent = modeAfterInitialise;
			PacketFormat = packetFormat;

			// Strobe Reset pin briefly to factory reset SX1231 chip
			ResetGpioPin.Write(GpioPinValue.High);
			Task.Delay(100);
			ResetGpioPin.Write(GpioPinValue.Low);
			Task.Delay(10);

			// Put the device into sleep mode so registers can be changed
			SetMode(RegOpModeMode.Sleep);

Most of the initialise method follows a similar pattern, checking parameters associated with a Register and only setting it if the cvalues are not all the default

// Configure RF Carrier frequency RegFrMsb, RegFrMid, RegFrLsb
if (frequency != FrequencyDefault)
{
	byte[] bytes = BitConverter.GetBytes((long)(frequency / RH_RFM69HCW_FSTEP));
	RegisterManager.WriteByte((byte)Registers.RegFrfMsb, bytes[2]);
	RegisterManager.WriteByte((byte)Registers.RegFrfMid, bytes[1]);
	RegisterManager.WriteByte((byte)Registers.RegFrfLsb, bytes[0]);
}

Some registers are a bit more complex to configure e.g. RegSyncConfig

// RegSyncConfig
if ((syncOn != SyncOnDefault) ||
	 (syncFifoFileCondition != SyncFifoFileConditionDefault) ||
	 (syncSize != SyncSizeDefault) ||
	 (syncTolerance != SyncToleranceDefault))
{
	byte regSyncConfigValue= 0b00000000;

	if (syncOn)
	{
		regSyncConfigValue |= 0b10000000;
	}
	
	regSyncConfigValue |= (byte)syncFifoFileCondition;

	regSyncConfigValue |= (byte)((syncSize - 1) << 3);
	regSyncConfigValue |= (byte)syncTolerance;
	RegisterManager.WriteByte((byte)Registers.RegSyncConfig, regSyncConfigValue);
}

I have just got to finish the code for RegFifoThresh, RegPacketConfig2 and the RegAesKey1-16 registers.

Other libraries for the RRFM69 support changing configuration while the application is running which significantly increases the complexity and number of test cases. My initial version will only support configuration on start-up.

RFM69 hat library Part11

RegisterManager Refactor

I had been meaning to refactor the code for accessing the registers of the RFM69CW/RFM69HCW module (based on the Semtech SX1231/SX1231H) registers for a while.

Adafruit RFM69 Radio Bonnet

There is now a lot less code in the startup.cs file and the code for configuring the RFM69 is more obvious

/*
    Copyright ® 2019 July devMobile Software, All Rights Reserved

	 MIT License

	 Permission is hereby granted, free of charge, to any person obtaining a copy
	 of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
	 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	 copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all
	 copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	 SOFTWARE

 */
namespace devMobile.IoT.Rfm69Hcw.RefactorRegisterManager
{
	using System;
	using System.Diagnostics;
	using System.Text;
	using System.Threading.Tasks;
	using Windows.ApplicationModel.Background;
	using Windows.Devices.Gpio;

	sealed class Rfm69HcwDevice
	{
		private GpioPin InterruptGpioPin = null;
		public RegisterManager RegisterManager = null; // Future refactor this will be made private

		public Rfm69HcwDevice(ChipSelectPin chipSelectPin, int resetPin, int interruptPin)
		{
			RegisterManager = new RegisterManager(chipSelectPin);

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

			// Interrupt pin for RX message & TX done notification 
			InterruptGpioPin = gpioController.OpenPin(interruptPin);
			resetGpioPin.SetDriveMode(GpioPinDriveMode.Input);

			InterruptGpioPin.ValueChanged += InterruptGpioPin_ValueChanged;
		}

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

			byte irqFlags = RegisterManager.ReadByte(0x28); // RegIrqFlags2
			Debug.WriteLine("{0:HH:mm:ss.fff} RegIrqFlags {1}", DateTime.Now, Convert.ToString((byte)irqFlags, 2).PadLeft(8, '0'));
			if ((irqFlags & 0b00000100) == 0b00000100)  // PayLoadReady set
			{
				// Read the length of the buffer
				byte numberOfBytes = RegisterManager.ReadByte(0x0);

				// Allocate buffer for message
				byte[] messageBytes = new byte[numberOfBytes];

				for (int i = 0; i < numberOfBytes; i++)
				{
					messageBytes[i] = RegisterManager.ReadByte(0x00); // RegFifo
				}

				string messageText = UTF8Encoding.UTF8.GetString(messageBytes);
				Debug.WriteLine("{0:HH:mm:ss} Received {1} byte message {2}", DateTime.Now, messageBytes.Length, messageText);
			}

			if ((irqFlags & 0b00001000) == 0b00001000)  // PacketSent set
			{
				RegisterManager.WriteByte(0x01, 0b00010000); // RegOpMode set ReceiveMode
				Debug.WriteLine("{0:HH:mm:ss.fff} Transmit-Done", DateTime.Now);
			}
		}

		public void RegisterDump()
		{
			RegisterManager.Dump(0x0, 0x40);
		}
	}
	

	public sealed class StartupTask : IBackgroundTask
	{
		private const int ResetPin = 25;
		private const int InterruptPin = 22;
		private Rfm69HcwDevice rfm69Device = new Rfm69HcwDevice(ChipSelectPin.CS1, ResetPin, InterruptPin);

		const double RH_RF6M9HCW_FXOSC = 32000000.0;
		const double RH_RFM69HCW_FSTEP = RH_RF6M9HCW_FXOSC / 524288.0;

		public void Run(IBackgroundTaskInstance taskInstance)
		{
			//rfm69Device.RegisterDump();

			// regOpMode standby
			rfm69Device.RegisterManager.WriteByte(0x01, 0b00000100);

			// BitRate MSB/LSB
			rfm69Device.RegisterManager.WriteByte(0x03, 0x34);
			rfm69Device.RegisterManager.WriteByte(0x04, 0x00);

			// Frequency deviation
			rfm69Device.RegisterManager.WriteByte(0x05, 0x02);
			rfm69Device.RegisterManager.WriteByte(0x06, 0x3d);

			// Calculate the frequency accoring to the datasheett
			byte[] bytes = BitConverter.GetBytes((uint)(915000000.0 / RH_RFM69HCW_FSTEP));
			Debug.WriteLine("Byte Hex 0x{0:x2} 0x{1:x2} 0x{2:x2} 0x{3:x2}", bytes[0], bytes[1], bytes[2], bytes[3]);
			rfm69Device.RegisterManager.WriteByte(0x07, bytes[2]);
			rfm69Device.RegisterManager.WriteByte(0x08, bytes[1]);
			rfm69Device.RegisterManager.WriteByte(0x09, bytes[0]);

			// RegRxBW
			rfm69Device.RegisterManager.WriteByte(0x19, 0x2a);

			// RegDioMapping1
			rfm69Device.RegisterManager.WriteByte(0x26, 0x01);

			// Setup preamble length to 16 (default is 3) RegPreambleMsb RegPreambleLsb
			rfm69Device.RegisterManager.WriteByte(0x2C, 0x0);
			rfm69Device.RegisterManager.WriteByte(0x2D, 0x10);

			// RegSyncConfig Set the Sync length and byte values SyncOn + 3 custom sync bytes
			rfm69Device.RegisterManager.WriteByte(0x2e, 0x90);

			// RegSyncValues1 thru RegSyncValues3
			rfm69Device.RegisterManager.WriteByte(0x2f, 0xAA);
			rfm69Device.RegisterManager.WriteByte(0x30, 0x2D);
			rfm69Device.RegisterManager.WriteByte(0x31, 0xD4);

			// RegPacketConfig1 Variable length with CRC on
			rfm69Device.RegisterManager.WriteByte(0x37, 0x90);

			rfm69Device.RegisterDump();

			while (true)
			{
				// Standby mode while loading message into FIFO
				rfm69Device.RegisterManager.WriteByte(0x01, 0b00000100);

				byte[] messageBuffer = UTF8Encoding.UTF8.GetBytes("hello world " + DateTime.Now.ToLongTimeString());
				rfm69Device.RegisterManager.WriteByte(0x0, (byte)messageBuffer.Length);
				rfm69Device.RegisterManager.Write(0x0, messageBuffer);

				// Transmit mode once FIFO loaded
				rfm69Device.RegisterManager.WriteByte(0x01, 0b00001100);

				Debug.WriteLine("{0:HH:mm:ss.fff} Send-Done", DateTime.Now);

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

I’ll modify the constructor reset pin support to see if I can get the Seegel Systeme hat working.

Register dump
Register 0x00 - Value 0X00 - Bits 00000000
Register 0x01 - Value 0X04 - Bits 00000100
Register 0x02 - Value 0X00 - Bits 00000000
Register 0x03 - Value 0X34 - Bits 00110100
…
Register 0x3e - Value 0X00 - Bits 00000000
Register 0x3f - Value 0X00 - Bits 00000000
Register 0x40 - Value 0X00 - Bits 00000000
19:58:52.828 Send-Done
19:58:53.022 RegIrqFlags 00001000
19:58:53.036 Transmit-Done
19:58:54.188 RegIrqFlags 01000110
19:58:54 Received 14 byte message Hello world:1
The thread 0xa10 has exited with code 0 (0x0).
The thread 0xf90 has exited with code 0 (0x0).
19:58:57.652 RegIrqFlags 01000110
19:58:57 Received 14 byte message Hello world:2
19:58:57.892 Send-Done
19:58:58.039 RegIrqFlags 00001000
19:58:58.053 Transmit-Done
19:59:01.115 RegIrqFlags 01000110
19:59:01 Received 14 byte message Hello world:3
19:59:02.936 Send-Done
19:59:03.083 RegIrqFlags 00001000
19:59:03.096 Transmit-Done
19:59:04.577 RegIrqFlags 01000110
19:59:04 Received 14 byte message Hello world:4
The thread 0xa5c has exited with code 0 (0x0).
19:59:08.001 Send-Done
19:59:08.122 RegIrqFlags 01001000
19:59:08.139 Transmit-Done
19:59:11.504 RegIrqFlags 01000110
19:59:11 Received 14 byte message Hello world:6
The thread 0xb18 has exited with code 0 (0x0).
19:59:13.079 Send-Done
19:59:13.226 RegIrqFlags 00001000
19:59:13.240 Transmit-Done
19:59:14.966 RegIrqFlags 01000110
19:59:14 Received 14 byte message Hello world:7

Based how my rate of progress improved when I did this on the RFM9X library I really should have done this much earlier.

RFM69 hat library Part5

Receive Basic: Rasmatic/RFM69-Arduino-Library

Next step was to extend my code to receive packets (no addressing or encryption). Initially I didn’t receive any messages as I had neglected to configure the variable length flag (RegPacketConfig bit 7) and had a typo in the RegRxBw register configuration.

/*
 Copyright ® 2019 June devMobile Software, All Rights Reserved

 MIT License

 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included in all
 copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE

*/
namespace devMobile.IoT.Rfm69Hcw.ReceiveBasic
{
	using System;
	using System.Diagnostics;
	using System.Runtime.InteropServices.WindowsRuntime;
	using System.Text;
	using System.Threading.Tasks;
	using Windows.ApplicationModel.Background;
	using Windows.Devices.Spi;
	using Windows.Devices.Gpio;

	public sealed class Rfm69HcwDevice
	{
		private SpiDevice Rfm69Hcw;
		private const byte RegisterAddressReadMask = 0X7f;
		private const byte RegisterAddressWriteMask = 0x80;

		public Rfm69HcwDevice(int chipSelectPin, int resetPin)
		{
			SpiController spiController = SpiController.GetDefaultAsync().AsTask().GetAwaiter().GetResult();
			var settings = new SpiConnectionSettings(chipSelectPin)
			{
				ClockFrequency = 500000,
				Mode = SpiMode.Mode0,
			};

			// Factory reset pin configuration
			GpioController gpioController = GpioController.GetDefault();
			GpioPin resetGpioPin = gpioController.OpenPin(resetPin);
			resetGpioPin.SetDriveMode(GpioPinDriveMode.Output);
			resetGpioPin.Write(GpioPinValue.Low);
			Task.Delay(100);
			resetGpioPin.Write(GpioPinValue.High);
			Task.Delay(100);
			resetGpioPin.Write(GpioPinValue.Low);

			Task.Delay(100);

			Rfm69Hcw = spiController.GetDevice(settings);
		}

		public Byte RegisterReadByte(byte address)
		{
			byte[] writeBuffer = new byte[] { address &= RegisterAddressReadMask };
			byte[] readBuffer = new byte[1];
			Debug.Assert(Rfm69Hcw != null);

			Rfm69Hcw.TransferSequential(writeBuffer, readBuffer);

			return readBuffer[0];
		}

		public ushort RegisterReadWord(byte address)
		{
			byte[] writeBuffer = new byte[] { address &= RegisterAddressReadMask };
			byte[] readBuffer = new byte[2];
			Debug.Assert(Rfm69Hcw != null);

			Rfm69Hcw.TransferSequential(writeBuffer, readBuffer);

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

		public byte[] RegisterRead(byte address, int length)
		{
			byte[] writeBuffer = new byte[] { address &= RegisterAddressReadMask };
			byte[] readBuffer = new byte[length];
			Debug.Assert(Rfm69Hcw != null);

			Rfm69Hcw.TransferSequential(writeBuffer, readBuffer);

			return readBuffer;
		}

		public void RegisterWriteByte(byte address, byte value)
		{
			byte[] writeBuffer = new byte[] { address |= RegisterAddressWriteMask, value };
			Debug.Assert(Rfm69Hcw != null);

			Rfm69Hcw.Write(writeBuffer);
		}

		public void RegisterWriteWord(byte address, ushort value)
		{
			byte[] valueBytes = BitConverter.GetBytes(value);
			byte[] writeBuffer = new byte[] { address |= RegisterAddressWriteMask, valueBytes[0], valueBytes[1] };
			Debug.Assert(Rfm69Hcw != null);

			Rfm69Hcw.Write(writeBuffer);
		}

		public void RegisterWrite(byte address, [ReadOnlyArray()] byte[] bytes)
		{
			byte[] writeBuffer = new byte[1 + bytes.Length];
			Debug.Assert(Rfm69Hcw != null);

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

			Rfm69Hcw.Write(writeBuffer);
		}

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

				Debug.WriteLine("Register 0x{0:x2} - Value 0X{1:x2} - Bits {2}", registerIndex, registerValue, Convert.ToString(registerValue, 2).PadLeft(8, '0'));
			}
		}
	}


	public sealed class StartupTask : IBackgroundTask
	{
		private const int ChipSelectLine = 1;
		private const int ResetLine = 25;
		private Rfm69HcwDevice rfm69Device = new Rfm69HcwDevice(ChipSelectLine, ResetLine);

		const double RH_RF6M9HCW_FXOSC = 32000000.0;
		const double RH_RFM69HCW_FSTEP = RH_RF6M9HCW_FXOSC / 524288.0;

		public void Run(IBackgroundTaskInstance taskInstance)
		{
			//rfm69Device.RegisterDump();

			// regOpMode standby
			rfm69Device.RegisterWriteByte(0x01, 0b00000100);

			// BitRate MSB/LSB
			rfm69Device.RegisterWriteByte(0x03, 0x34);
			rfm69Device.RegisterWriteByte(0x04, 0x00);

			// Calculate the frequency accoring to the datasheett
			byte[] bytes = BitConverter.GetBytes((uint)(915000000.0 / RH_RFM69HCW_FSTEP));
			Debug.WriteLine("Byte Hex 0x{0:x2} 0x{1:x2} 0x{2:x2} 0x{3:x2}", bytes[0], bytes[1], bytes[2], bytes[3]);
			rfm69Device.RegisterWriteByte(0x07, bytes[2]);
			rfm69Device.RegisterWriteByte(0x08, bytes[1]);
			rfm69Device.RegisterWriteByte(0x09, bytes[0]);

			// RegRxBW
			rfm69Device.RegisterWriteByte(0x19, 0x2a);

			// Setup preamble length to 16 (default is 3) RegPreambleMsb RegPreambleLsb
			rfm69Device.RegisterWriteByte(0x2C, 0x0);
			rfm69Device.RegisterWriteByte(0x2D, 0x10);

			// RegSyncConfig Set the Sync length and byte values SyncOn + 3 custom sync bytes
			rfm69Device.RegisterWriteByte(0x2e, 0x90);

			// RegSyncValues1 thru RegSyncValues3
			rfm69Device.RegisterWriteByte(0x2f, 0xAA);
			rfm69Device.RegisterWriteByte(0x30, 0x2D);
			rfm69Device.RegisterWriteByte(0x31, 0xD4);

			// RegPacketConfig1 Variable length with CRC on
			rfm69Device.RegisterWriteByte(0x37, 0x90);

			rfm69Device.RegisterWriteByte(0x01, 0b00010000); // RegOpMode set ReceiveMode

			rfm69Device.RegisterDump();

			while (true)
			{
				// Wait until a packet is received, no timeouts in PoC
				Debug.WriteLine("Receive-Wait");
				byte IrqFlags = rfm69Device.RegisterReadByte(0x28); // RegIrqFlags2
				while ((IrqFlags & 0b00000100) == 0)  // wait until PayLoadReady set
				{
					Task.Delay(20).Wait();
					IrqFlags = rfm69Device.RegisterReadByte(0x28); // RegIrqFlags2
					//Debug.WriteLine(string.Format("RegIrqFlags {0}", Convert.ToString((byte)IrqFlags, 2).PadLeft(8, '0')));
					Debug.Write(".");
				}
				Debug.WriteLine("");

				// Rwad the length
				byte numberOfBytes = rfm69Device.RegisterReadByte(0x0); 

				// Allocate buffer for message
				byte[] messageBytes = new byte[numberOfBytes];

				for (int i = 0; i < numberOfBytes; i++)
				{
					messageBytes[i] = rfm69Device.RegisterReadByte(0x00); // RegFifo
				}

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

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

I modified the Arduino application to transmit a message (with a counter so I could spot dropped messages) every second

#include <SPI.h>
#include <stdio.h>
#include <RMRFM69.h>

#define SENDER_DETECT_PIN 4

RMRFM69 radio(SPI, 10, 2, 9);

byte counter = 0 ;

void setup() 
{
  Serial.begin(9600);

  pinMode(SENDER_DETECT_PIN, INPUT_PULLUP);  
  
  radio.Modulation     = FSK;
  radio.COB            = RFM69;
  radio.Frequency      = 915000;
  radio.OutputPower    = 10+18;          //10dBm OutputPower
  radio.PreambleLength = 16;             //16Byte preamble
  radio.FixedPktLength = false;          //packet in message which need to be send
  radio.CrcDisable     = false;          //CRC On
  radio.AesOn          = false;
  radio.SymbolTime     = 416000;         //2.4Kbps
  radio.Devation       = 35;             //35KHz for devation
  radio.BandWidth      = 100;            //100KHz for bandwidth
  radio.SyncLength     = 3;              //
  radio.SyncWord[0]    = 0xAA;
  radio.SyncWord[1]    = 0x2D;
  radio.SyncWord[2]    = 0xD4;

  radio.vInitialize();
  
  if (digitalRead(SENDER_DETECT_PIN) == LOW)
  {
    Serial.println("RX start");
    radio.vGoRx();
  }
  else
  {
    Serial.println("TX start");
    //radio.vGoTx();    
  }
  radio.dumpRegisters(Serial);
}

void loop() 
{
  char messageIn[128] = {""};
  char messageOut[32]= {"Hello world:"};

  if (digitalRead(SENDER_DETECT_PIN) == LOW)
  {
    if(radio.bGetMessage(messageIn)!=0)
    { 
      Serial.print("MessageIn:");
      Serial.println(messageIn);
    }    
  }
  else
  {  
    Serial.print("MessageOut:") ;
    itoa(counter,&messageOut[strlen( messageOut)],10);
    Serial.println(messageOut);
    if (!radio.bSendMessage(messageOut, strlen(messageOut)))
    {
      Serial.println("bSendMessage failed");
    }
    counter++;
    delay(1000);
  }
}
Arduino device in receive mode

With the Arduino device transmitting the debug output in Visual Studio looked like this. It does seem a bit odd that the “Receive-Wait” is slowly getting longer.

Register dump
Register 0x00 - Value 0X00 - Bits 00000000
Register 0x01 - Value 0X10 - Bits 00010000
…
Register 0x3c - Value 0X0f - Bits 00001111
Register 0x3d - Value 0X02 - Bits 00000010
Receive-Wait
The thread 0x990 has exited with code 0 (0x0).
Received 13 byte message Hello world:0
Receive-Done
Receive-Wait
.............................................
Received 13 byte message Hello world:1
Receive-Done
Receive-Wait
...........................................
Received 13 byte message Hello world:2
Receive-Done
Receive-Wait
............................................
Received 13 byte message Hello world:3
Receive-Done
Receive-Wait
............................................
Received 13 byte message Hello world:4
Receive-Done
Receive-Wait
.............................................
Received 13 byte message Hello world:5
Receive-Done
Receive-Wait
............................................
Received 13 byte message Hello world:6
Receive-Done
Receive-Wait
............................................

I ran the client for several hours and it didn’t appear to drop any messages. Next step is to covert the receive and transmit code to use interrupts.