nanoFramework nRF24L01 library Part2

After sorting out Serial Peripheral Interface(SPI) connectivity the next step porting my GHI Electronics TinyCLR V2 library to the nanoFramework was rewriting the initialisation code. Overall changes were minimal as the nanoFramework similar methods to the TinyCLR V2 ones.

The Tiny CLR SPI and interrupt port configuration (note the slightly different interrupt port configuration)

if (gpio == null)
{
   Debug.WriteLine("GPIO Initialization failed.");
}
else
{
   _cePin = gpio.OpenPin(chipEnablePin);
   _cePin.SetDriveMode(GpioPinDriveMode.Output);
   _cePin.Write(GpioPinValue.Low);

   _irqPin = gpio.OpenPin((byte)interruptPin);
   _irqPin.SetDriveMode(GpioPinDriveMode.InputPullUp);
   _irqPin.Write(GpioPinValue.High);
   _irqPin.ValueChanged += _irqPin_ValueChanged;
}

try
{
   var settings = new SpiConnectionSettings()
   {
      ChipSelectType = SpiChipSelectType.Gpio,
      ChipSelectLine = gpio.OpenPin(chipSelectPin),
      Mode = SpiMode.Mode0,
      ClockFrequency = clockFrequency,
      ChipSelectActiveState = false,
   };

   SpiController controller = SpiController.FromName(spiPortName);
   _spiPort = controller.GetDevice(settings);
}
catch (Exception ex)
{
   Debug.WriteLine("SPI Initialization failed. Exception: " + ex.Message);
   return;
}

The nanoFramework SPI and interrupt port configuration (note the slightly different SPI port configuration)

public void Initialize(string spiPortName, int chipEnablePin, int chipSelectPin, int interruptPin, int clockFrequency = 2000000)
{
   var gpio = GpioController.GetDefault();

   if (gpio == null)
   {
      Debug.WriteLine("GPIO Initialization failed.");
   }
   else
   {
      _cePin = gpio.OpenPin(chipEnablePin);
      _cePin.SetDriveMode(GpioPinDriveMode.Output);
      _cePin.Write(GpioPinValue.Low);

      _irqPin = gpio.OpenPin((byte)interruptPin);
      _irqPin.SetDriveMode(GpioPinDriveMode.InputPullUp);
      _irqPin.ValueChanged += irqPin_ValueChanged;
   }

   try
   {
      var settings = new SpiConnectionSettings(chipSelectPin)
      {
         ClockFrequency = clockFrequency,
         Mode = SpiMode.Mode0,
         SharingMode = SpiSharingMode.Shared,
      };

      _spiPort = SpiDevice.FromId(spiPortName, settings);
   }
   catch (Exception ex)
   {
      Debug.WriteLine("SPI Initialization failed. Exception: " + ex.Message);
   return;
   }

The error handling of the initialise method is broken. If the some of the GPIO or SPI port configuration fails a message is displayed in the Debug output but the caller is not notified.

I’m using a Netduino 3 Wifi as the SPI port configuration means I can use a standard Arduino shield to connect up the NRF24L01 wireless module without any jumpers

Netduino 3 Wifi and embedded coolness shield

I have applied the PowerLevel fix from the TinyCLR and Meadow libraries but worry that there maybe other issues.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
Address: Dev01
PowerLevel: 2
IsAutoAcknowledge: True
Channel: 15
DataRate: 2
IsDynamicAcknowledge: False
IsDynamicPayload: True
IsEnabled: False
Frequency: 2415
IsInitialized: True
IsPowered: True
00:00:15-TX 9 byte message hello 255
Data Sent!
00:00:15-TX Succeeded!

Based on my experiences porting the library to three similar platforms and debugging it on two others I’m considering writing my own compile-time platform portable library.

nanoFramework nRF24L01 library Part1

After porting then debugging Windows 10 IoT Core, .NetMF, Wilderness Labs Meadow and GHI Electronics TinyCLR nRF24L01P libraries I figured yet another port, this time to a nanoFramework powered devices should be low risk.

My initial test rig uses a Netduino 3 Wifi and an Embedded Coolness nRF24 shield as I didn’t need to use jumper wires.

//---------------------------------------------------------------------------------
// 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.
//
//---------------------------------------------------------------------------------
#define NETDUINO3_WIFI   // nanoff --target NETDUINO3_WIFI --update

namespace devMobile.IoT.nRf24L01.ModuleSPI
{
   using System;
   using System.Threading;
   using System.Diagnostics;
   using System.Text;
   using Windows.Devices.Gpio;
   using Windows.Devices.Spi;

   public class Program
   {
      const byte SETUP_AW = 0x03;
      const byte RF_CH = 0x05;
      const byte RX_ADDR_P0 = 0x0A;
      const byte R_REGISTER = 0b00000000;
      const byte W_REGISTER = 0b00100000;
      const string P0_Address = "ZYXWV";

#if NETDUINO3_WIFI
      private const string SpiBusId = "SPI2";
#endif

      public static void Main()
      {
#if NETDUINO3_WIFI
         // Arduino D7->PD7
         int chipSelectPinNumber = PinNumber('A', 1);
#endif
         Debug.WriteLine("devMobile.IoT.nRf24L01.ModuleSPI starting");

         Debug.WriteLine(Windows.Devices.Spi.SpiDevice.GetDeviceSelector());

         try
         {
            GpioController gpioController = GpioController.GetDefault();

            var settings = new SpiConnectionSettings(chipSelectPinNumber)
            {
               ClockFrequency = 2000000,
               Mode = SpiMode.Mode0,
               SharingMode = SpiSharingMode.Shared,
            };

            using (SpiDevice device = SpiDevice.FromId(SpiBusId, settings))
            {
               Debug.WriteLine("nrf24L01Device Device...");
               if (device == null)
               {
                  Debug.WriteLine("nrf24L01Device == null");
               }

               Thread.Sleep(100);

               Debug.WriteLine("ConfigureSpiPort Done...");
               Debug.WriteLine("");

               Thread.Sleep(500);
               try
               {
                  // Read the Address width
                  Debug.WriteLine("Read address width");
                  byte[] txBuffer1 = new byte[] { SETUP_AW | R_REGISTER, 0x0 };
                  byte[] rxBuffer1 = new byte[txBuffer1.Length];

                  Debug.WriteLine(" nrf24L01Device.TransferFullDuplex...SETUP_AW");
                  Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer1));
                  device.TransferFullDuplex(txBuffer1, rxBuffer1);
                  Debug.WriteLine(" rxBuffer:" + BitConverter.ToString(rxBuffer1));

                  // Extract then adjust the address width
                  byte addressWidthValue = rxBuffer1[1];
                  addressWidthValue &= 0b00000011;
                  addressWidthValue += 2;
                  Debug.WriteLine($"Address width 0x{SETUP_AW:x2} - Value 0X{rxBuffer1[1]:x2} Value adjusted {addressWidthValue}");
                  Debug.WriteLine("");

                  // Write Pipe0 Receive address
                  Debug.WriteLine($"Write Pipe0 Receive Address {P0_Address}");
                  byte[] txBuffer2 = new byte[addressWidthValue + 1];
                  byte[] rxBuffer2 = new byte[txBuffer2.Length];
                  txBuffer2[0] = RX_ADDR_P0 | W_REGISTER;
                  Array.Copy(Encoding.UTF8.GetBytes(P0_Address), 0, txBuffer2, 1, addressWidthValue);

                  Debug.WriteLine(" nrf24L01Device.Write...RX_ADDR_P0");
                  Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer2));
                  device.TransferFullDuplex(txBuffer2, rxBuffer2);
                  Debug.WriteLine("");

                  // Read Pipe0 Receive address
                  Debug.WriteLine("Read Pipe0 Receive address");
                  byte[] txBuffer3 = new byte[addressWidthValue + 1];
                  txBuffer3[0] = RX_ADDR_P0 | R_REGISTER;
                  byte[] rxBuffer3 = new byte[txBuffer3.Length];

                  Debug.WriteLine(" nrf24L01Device.TransferFullDuplex...RX_ADDR_P0");
                  Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer3));
                  device.TransferFullDuplex(txBuffer3, rxBuffer3);
                  Debug.WriteLine(" rxBuffer:" + BitConverter.ToString(rxBuffer3));
                  Debug.WriteLine($"Address 0x{RX_ADDR_P0:x2} Address {UTF8Encoding.UTF8.GetString(rxBuffer3, 1, addressWidthValue)}");
                  Debug.WriteLine("");

                  // Read the RF Channel
                  Debug.WriteLine("RF Channel read 1");
                  byte[] txBuffer4 = new byte[] { RF_CH | R_REGISTER, 0x0 };
                  byte[] rxBuffer4 = new byte[txBuffer4.Length];

                  Debug.WriteLine(" nrf24L01Device.TransferFullDuplex...RF_CH");
                  Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer4));
                  device.TransferFullDuplex(txBuffer4, rxBuffer4);
                  Debug.WriteLine(" rxBuffer:" + BitConverter.ToString(rxBuffer4));

                  byte rfChannel1 = rxBuffer4[1];
                  Debug.WriteLine($"RF Channel 1 0x{RF_CH:x2} - Value 0X{rxBuffer4[1]:x2} - Value adjusted {rfChannel1+2400}");
                  Debug.WriteLine("");

                  // Write the RF Channel
                  Debug.WriteLine("RF Channel write");
                  byte[] txBuffer5 = new byte[] { RF_CH | W_REGISTER, rfChannel1+=1};
                  byte[] rxBuffer5 = new byte[txBuffer5.Length];

                  Debug.WriteLine(" nrf24L01Device.Write...RF_CH");
                  Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer5));
                  //device.Write(txBuffer5);
                  device.TransferFullDuplex(txBuffer5, rxBuffer5);
                  Debug.WriteLine("");

                  // Read the RF Channel
                  Debug.WriteLine("RF Channel read 2");
                  byte[] txBuffer6 = new byte[] { RF_CH | R_REGISTER, 0x0 };
                  byte[] rxBuffer6 = new byte[txBuffer6.Length];

                  Debug.WriteLine(" nrf24L01Device.TransferFullDuplex...RF_CH");
                  Debug.WriteLine(" txBuffer:" + BitConverter.ToString(txBuffer6));
                  device.TransferFullDuplex(txBuffer6, rxBuffer6);
                  Debug.WriteLine(" rxBuffer:" + BitConverter.ToString(rxBuffer6));

                  byte rfChannel2 = rxBuffer6[1];
                  Debug.WriteLine($"RF Channel 2 0x{RF_CH:x2} - Value 0X{rxBuffer6[1]:x2} - Value adjusted {rfChannel2+2400}");
                  Debug.WriteLine("");
               }
               catch (Exception ex)
               {
                  Debug.WriteLine("Configure Port0 " + ex.Message);
               }
            }
         }
         catch (Exception ex)
         {
            Debug.WriteLine(ex.Message);
         }
      }

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

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

After bit of tinkering with SPI configuration options and checking device.Write vs. device.TransferFullDuplex usage. I can reliably read and write my nRF24L01 device’s receive port address and channel configuration.

devMobile.IoT.nRf24L01.ModuleSPI starting
SPI1,SPI2,SPI3,SPI4
nrf24L01Device Device...
ConfigureSpiPort Done...

Read address width
 nrf24L01Device.TransferFullDuplex...SETUP_AW
 txBuffer:03-00
 rxBuffer:0E-03
Address width 0x03 - Value 0X03 Value adjusted 5

Write Pipe0 Receive Address ZYXWV
 nrf24L01Device.Write...RX_ADDR_P0
 txBuffer:2A-5A-59-58-57-56

Read Pipe0 Receive address
 nrf24L01Device.TransferFullDuplex...RX_ADDR_P0
 txBuffer:0A-00-00-00-00-00
 rxBuffer:0E-5A-59-58-57-56
Address 0x0A Address ZYXWV

RF Channel read 1
 nrf24L01Device.TransferFullDuplex...RF_CH
 txBuffer:05-00
 rxBuffer:0E-02
RF Channel 1 0x05 - Value 0X02 - Value adjusted 2402

RF Channel write
 nrf24L01Device.Write...RF_CH
 txBuffer:25-03

RF Channel read 2
 nrf24L01Device.TransferFullDuplex...RF_CH
 txBuffer:05-00
 rxBuffer:0E-03
RF Channel 2 0x05 - Value 0X03 - Value adjusted 2403

The thread '<No Name>' (0x1) has exited with code 0 (0x0).
Done.

Next step is to port my TinyCLR nRF24L01 library which is based on the Techfoonina Windows 10 IoT Core port which is based on .NetMF library by Gralin.

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!");
      }
   }
}

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.

nanoFramework RAK811 LoRaWAN library Part7

Now with added callbacks

After building a nanoFramwork library “inspired” by the RakWireless Arduino library(which has some issues) I figured it would be good to refactor the library to be more asynchronous with event handlers for send confirmation (if configured) and received messages.

If the RAK811 module is initialised, and connects to the network successfully, the application sends “48656c6c6f204c6f526157414e” (“hello LoRaWAN”) every 5 minutes.

STM32F691Discovery with EVB plugged into Arduino headers

The code application code now has a lot more compile time options for network configuration and payload format.

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

   using devMobile.IoT.LoRaWan;

   public class Program
   {
#if ST_STM32F769I_DISCOVERY
      private const string SerialPortId = "COM6";
#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");

         Debug.WriteLine($"Ports :{Windows.Devices.SerialCommunication.SerialDevice.GetDeviceSelector()}");

         try
         {
            using ( Rak811LoRaWanDevice device = new Rak811LoRaWanDevice())
            {
               result = device.Initialise(SerialPortId, 9600, SerialParity.None, 8, SerialStopBitCount.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} Send Timeout:{SendTimeout: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(new TimeSpan( 0,0,5));

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

                  Thread.Sleep(new TimeSpan(0, 5, 0));

                  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)}");
      }
   }
}

The debugging output with the Rak811LoRaWanDevice class diagnostics off

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.IoT.Rak811LoRaWanDeviceClient starting
Ports :COM5,COM6
12:00:51 Region AS923
12:00:51 ADR On
12:00:51 Confirmed
12:00:52 OTAA
12:00:52 Join start Timeout:00:00:10
12:00:59 Join finish
12:00:59 Send Timeout:00:00:10 port:1 payload BCD:48656c6c6f204c6f526157414e
12:01:02 Send Confirm RSSI:-79 SNR:9
12:01:07 Sleep
12:06:07 Wakeup
12:06:07 Send Timeout:00:00:10 port:1 payload BCD:48656c6c6f204c6f526157414e
12:06:08 Send Confirm RSSI:-65 SNR:8
12:06:13 Sleep
12:11:13 Wakeup
12:11:13 Send Timeout:00:00:10 port:1 payload BCD:48656c6c6f204c6f526157414e
12:11:15 Send Confirm RSSI:-60 SNR:7
12:11:15 Receive Message RSSI:-60 SNR:7 Port:5 Payload:48656c6c6f PayLoadBytes:48-65-6C-6C-6F
12:11:20 Sleep
TTN OTAA Connection + RX&TX

Some commands are quite quick to respond e.g. setting the Region, Sleep, and Wakeup. Others, take quite a while e.g. Join, Send, WorkMode so they have separate timeout configurations.

The code is approaching beat and I’ll be testing and fixing bugs for the next couple of days.

nanoFramework RAK811 LoRaWAN library Part6

Inspired by the Arduino Library

After successful proof of concept projects I have build a nanoFramwork library “inspired” by the RakWireless Arduino library.

The initial version only supports my RAK811 LPWAN Evaluation Board(EVB) and STM32F691DISCOVERY based test rig It handles failures, displays error codes/messages, but doesn’t handle all timeouts.

If the RAK811 module is initialised, then connects to the network successfully, the application sends “48656c6c6f204c6f526157414e” (“hello LoRaWAN”) every 20 seconds.

STM32F691Discovery with EVB plugged into Arduino headers

The code application code is now a lot smaller & simpler

   public class Program
   {
#if ST_STM32F769I_DISCOVERY
      private const string SerialPortId = "COM6";
#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 byte MessagePort = 1;
      private const string Payload = "48656c6c6f204c6f526157414e"; // Hello LoRaWAN

      public static void Main()
      {
         Result result;

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

         Debug.WriteLine(Windows.Devices.SerialCommunication.SerialDevice.GetDeviceSelector());

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

               result = device.Region("AS923");
               if (result != Result.Success)
               {
                  Debug.WriteLine($"Region failed {result}");
                  return;
               }

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

#if ABP
               result = device.AbpInitialise(devAddress, nwksKey, appsKey);
               if (result != Result.Success)
               {
                  Debug.WriteLine($"ABP Initialise failed {result}");
                  return;
               }
#endif

               result = device.Join(new TimeSpan(0,0,10));
               if (result != Result.Success)
               {
                  Debug.WriteLine($"Join failed {result}");
                  return;
               }

               while (true)
               {
                  result = device.Send(MessagePort, Payload);
                  if (result != Result.Success)
                  {
                     Debug.WriteLine($"Send failed {result}");
                  }

                  result = device.Sleep();
                  if (result != Result.Success)
                  {
                     Debug.WriteLine($"Sleep failed {result}");
                     return;
                  }

                  Thread.Sleep(20000);

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

I compared the debugging output with confirmations off

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
 devMobile.IoT.Rak811LoRaWanDeviceClient starting
COM5,COM6
01:11:13 lora:work_mode
TX: send 32 bytes 32 via COM6
RX 01:11:14:UART1 work mode: RUI_UART_NORAMAL
Current work_mode:LoRaWAN, join_mode:OTAA, Class: A
Initialization OK 

01:11:15 lora:region
TX: send 33 bytes 33 via COM6
RX 01:11:16:OK 

01:11:16 lora:join_mode
TX: send 32 bytes 32 via COM6
RX 01:11:17:OK 

01:11:18 lora:dev_eui
TX: send 45 bytes 45 via COM6
RX 01:11:19:OK 

01:11:19 lora:app_eui
TX: send 45 bytes 45 via COM6
RX 01:11:20:OK 

01:11:21 lora:app_key
TX: send 61 bytes 61 via COM6
RX 01:11:22:OK 

01:11:22 join
TX: send 9 bytes 9 via COM6
RX 01:11:29:OK Join Success

TX: send 43 bytes to output stream.
TX: 43 bytes via COM6
TX: send 43 bytes to output stream.
TX: 43 bytes via COM6
RX :OK 

TX: send 43 bytes to output stream.
TX: 43 bytes via COM6
RX :OK 
at+recv=1,-54,9,5:48656c6c6f

TX: send 43 bytes to output stream.
TX: 43 bytes via COM6

TX: send 43 bytes to output stream.
TX: 43 bytes via COM6
RX :OK 
at+recv=2,-51,7,5:48656c6c6f

TX: send 43 bytes to output stream.
TX: 43 bytes via COM6

Then with confirmations on (note the at+recv=0,-59,7,0) and received messages (at+recv=23,-53,8,5:48656c6c6f)

devMobile.IoT.Rak811LoRaWanDeviceClient starting
COM5,COM6
01:20:54 lora:work_mode
TX: send 32 bytes 32 via COM6
RX 01:20:56:UART1 work mode: RUI_UART_NORAMAL
Current work_mode:LoRaWAN, join_mode:OTAA, Class: A
Initialization OK 

01:20:56 lora:region
TX: send 33 bytes 33 via COM6
RX 01:20:57:OK 

01:20:58 lora:join_mode
TX: send 32 bytes 32 via COM6
RX 01:20:59:OK 

01:20:59 lora:dev_eui
TX: send 45 bytes 45 via COM6
RX 01:21:00:OK 

01:21:01 lora:app_eui
TX: send 45 bytes 45 via COM6
RX 01:21:02:OK 

01:21:02 lora:app_key
TX: send 61 bytes 61 via COM6
RX 01:21:03:OK 

01:21:04 join
TX: send 9 bytes 9 via COM6
RX 01:21:11:OK Join Success

01:21:11 lora:confirm
TX: send 30 bytes 30 via COM6
RX 01:21:12:OK 

TX: send 43 bytes to output stream.
TX: 43 bytes via COM6
TX: send 43 bytes to output stream.
TX: 43 bytes via COM6
TX: send 43 bytes to output stream.
TX: 43 bytes via COM6
RX :OK 
at+recv=23,-53,8,5:48656c6c6f

TX: send 43 bytes to output stream.
TX: 43 bytes via COM6

TX: send 43 bytes to output stream.
TX: 43 bytes via COM6
RX :OK 
at+recv=0,-59,7,0

In the Visual Studio 2019 debug output I could see the responses to the AT Commands and especially the lack of handling of downlink messages and confirmations from the network.

The next step is to implement timeouts for when operations fail or the module doesn’t respond. Then extend the code to support the receiving of messages as a class A device (missing for the RAK arduino library). I wonder how this will work for when the module is configured as a class C device which can receive messages at any time.

Some commands are quite quick to respond e.g. setting the Region, Sleep, and Wakeup so are most probably ok running synchronously. Other commands can take quite a while e.g. Join, Send, WorkMode so maybe these need to be asynchronous (along with the receiving of confirmations and messages ).

The code is not suitable for production but it confirmed my new approach worked.

nanoFramework RAK811 LoRaWAN library Part5

Nasty ABP connect

After a successful Over The Air Activation(OTAA) with my RAK811 LPWAN Evaluation Board(EVB) and STM32F691DISCOVERY based test rig. I figured for completeness an Activation by Personalization (ABP) would be a good.

My ABP implementation is based on my OTAA one so is pretty “nasty”. Again, I assumed that there would be no timeouts or failures and I only send one message BCD “48656c6c6f204c6f526157414e” (“hello LoRaWAN”) every 20 seconds.

STM32F691Discovery with EVB plugged into Arduino headers

I created a new ABP device

Things Network ABP configuration

Then I configured the RAK811 module for LoRaWAN

// Set the Working mode to LoRaWAN
bytesWritten = outputDataWriter.WriteString("at+set_config=lora:work_mode:0rn");
Debug.WriteLine($"TX: work_mode {outputDataWriter.UnstoredBufferLength} bytes to output stream.");
txByteCount = outputDataWriter.Store();
Debug.WriteLine($"TX: {txByteCount} bytes via {serialDevice.PortName}");

// Read the response
bytesRead = inputDataReader.Load(128);
if (bytesRead > 0)
{
   string response = inputDataReader.ReadString(bytesRead);
   Debug.WriteLine($"RX sync:{response}");
}

Then sequentially stepped through the necessary configuration to join the The Things Network(TTN) network

// Set the JoinMode to ABP
bytesWritten = outputDataWriter.WriteString($"at+set_config=lora:join_mode:1\r\n");
Debug.WriteLine($"TX: join_mode {outputDataWriter.UnstoredBufferLength} bytes to output stream.");
txByteCount = outputDataWriter.Store();
Debug.WriteLine($"TX: {txByteCount} bytes via {serialDevice.PortName}");

// Read the response
bytesRead = inputDataReader.Load(128);
if (bytesRead > 0)
{
   String response = inputDataReader.ReadString(bytesRead);
   Debug.WriteLine($"RX :{response}");
}

// Set the device address
bytesWritten = outputDataWriter.WriteString($"at+set_config=lora:dev_addr:{devAddress}\r\n");
Debug.WriteLine($"TX: dev_addr {outputDataWriter.UnstoredBufferLength} bytes to output stream.");
txByteCount = outputDataWriter.Store();
Debug.WriteLine($"TX: {txByteCount} bytes via {serialDevice.PortName}");

// Read the response
bytesRead = inputDataReader.Load(128);
if (bytesRead > 0)
   {
   String response = inputDataReader.ReadString(bytesRead);
   Debug.WriteLine($"RX :{response}");
   }
...

After making a few fixes to my code and tweaking some settings I could see data in the TTN Console.

ABP Device data uplink

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

In the Visual Studio 2019 debug output I could see the AT Command responses from were getting truncated in odd ways so I need to be careful how they are processed.

The thread '<No Name>' (0x2) has exited with code 0 (0x0).
devMobile.IoT.Rak811.NetworkJoinABP starting
COM5,COM6
TX: work_mode 32 bytes to output stream.
TX: 32 bytes via COM6
RX :UART1 work mode: RUI_UART_NORAMAL
Current work_mode:LoRaWAN, join_mode:ABP, Class: A
Initialization OK 

TX: region 33 bytes to output stream.
TX: 33 bytes via COM6
RX :OK 

TX: join_mode 32 bytes to output stream.
TX: 32 bytes via COM6
RX :OK 

TX: dev_addr 38 bytes to output stream.
TX: 38 bytes via COM6
RX :OK 

TX: nwks_key 62 bytes to output stream.
TX: 62 bytes via COM6
RX :OK 

TX: apps_key 62 bytes to output stream.
TX: 62 bytes via COM6
RX :OK 

TX: confirm 30 bytes to output stream.
TX: 30 bytes via COM6
RX :OK 

TX: join 9 bytes to output stream.
TX: 9 bytes via COM6
TX: send 43 bytes to output stream.
TX: 43 bytes via COM6
TX: send 43 bytes to output stream.
TX: 43 bytes via COM6
RX :OK Jo
TX: send 43 bytes to output stream.
TX: 43 bytes via COM6
RX :in Su
TX: send 43 bytes to output stream.
TX: 43 bytes via COM6
RX :ccess
TX: send 43 bytes to output stream.
TX: 43 bytes via COM6
RX :
OK 
TX: send 43 bytes to output stream.
TX: 43 bytes via COM6
RX :
OK 

The next step is to get rework the code to process responses to the AT commands in a smarter way and extract error codes when an operation fails.

nanoFramework RAK811 LoRaWAN library Part4

Nasty OTAA connect

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

STM32F691Discovery with EVB plugged into Arduino headers

My Over the Air Activation(OTAA) implementation is pretty “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
bytesWritten = outputDataWriter.WriteString("at+set_config=lora:work_mode:0\r\n");
Debug.WriteLine($"TX: work_mode {outputDataWriter.UnstoredBufferLength} bytes to output stream.");
txByteCount = outputDataWriter.Store();
Debug.WriteLine($"TX: {txByteCount} bytes via {serialDevice.PortName}");

// Read the response
bytesRead = inputDataReader.Load(128);
if (bytesRead > 0)
{
   string response = inputDataReader.ReadString(bytesRead);
   Debug.WriteLine($"RX sync:{response}");
}

Then just sequentially stepped through the necessary configuration to join the TTN network

// Set the Region to AS923
bytesWritten = outputDataWriter.WriteString("at+set_config=lora:region:AS923\r\n");
Debug.WriteLine($"TX: region {outputDataWriter.UnstoredBufferLength} bytes to output stream.");
txByteCount = outputDataWriter.Store();
Debug.WriteLine($"TX: {txByteCount} bytes via {serialDevice.PortName}");

// Read the response
bytesRead = inputDataReader.Load(128);
if (bytesRead > 0)
{
   String response = inputDataReader.ReadString(bytesRead);
   Debug.WriteLine($"RX sync:{response}");
}

// Set the JoinMode
bytesWritten = outputDataWriter.WriteString($"at+set_config=lora:join_mode:0\r\n");
Debug.WriteLine($"TX: join_mode {outputDataWriter.UnstoredBufferLength} bytes to output stream.");
txByteCount = outputDataWriter.Store();
Debug.WriteLine($"TX: {txByteCount} bytes via {serialDevice.PortName}");

// Read the response
bytesRead = inputDataReader.Load(128);
if (bytesRead > 0)
{
   String response = inputDataReader.ReadString(bytesRead);
   Debug.WriteLine($"RX sync:{response}");
}

// OTAA set the devEUI
bytesWritten = outputDataWriter.WriteString($"at+set_config=lora:dev_eui:{devEui}\r\n");
Debug.WriteLine($"TX: dev_eui {outputDataWriter.UnstoredBufferLength} bytes to output stream.");
txByteCount = outputDataWriter.Store();
Debug.WriteLine($"TX: {txByteCount} bytes via {serialDevice.PortName}");

// Read the response
bytesRead = inputDataReader.Load(128);
if (bytesRead > 0)
{
   String response = inputDataReader.ReadString(bytesRead);
   Debug.WriteLine($"RX sync:{response}");
}
...

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
COM5,COM6
TX: work_mode 32 bytes to output stream.
TX: 32 bytes via COM6
RX sync:UART1 work mode: RUI_UART_NORAMAL
Current work_mode:LoRaWAN, join_mode:OTAA, Class: A
Initialization OK 

TX: region 33 bytes to output stream.
TX: 33 bytes via COM6
RX sync:OK 

TX: join_mode 32 bytes to output stream.
TX: 32 bytes via COM6
RX sync:OK 

TX: dev_eui 45 bytes to output stream.
TX: 45 bytes via COM6
RX sync:OK 

TX: app_eui 45 bytes to output stream.
TX: 45 bytes via COM6
RX sync:OK 

TX: app_key 61 bytes to output stream.
TX: 61 bytes via COM6
RX sync:OK 

TX: confirm 30 bytes to output stream.
TX: 30 bytes via COM6
RX sync:OK 

TX: join 9 bytes to output stream.
TX: 9 bytes via COM6
RX sync:
RX sync:
RX sync:
RX sync:
RX sync:OK Join Success

TX: send 43 bytes to output stream.
TX: 43 bytes via COM6
TX: send 43 bytes to output stream.
TX: 43 bytes via COM6
RX sync:OK 
at+recv=0,-59,9,0

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

I then modified the confirmed flag and in the TTN console I could see how they were processed differently.

Confirmed messages
Unconfirmed messages

I could receive messages but as the RAK 811 module can be configured to be a Class C device there didn’t appear to be a way to receive a message without sending one which seemed a bit odd.

The next step is to get Authentication By Personalisation(ABP) working.

RAK811 LPWAN EVB Part3

Invalidating the warranty…

I wanted the RAK811 LPWAN Evaluation Board(EVB) -AS923 to work with selection of my Arduino and nanoFramework devices. The first decision was which of the hardware serial port (D0,D1) or the software serial port (D10,D11) should be connected to P1?

To use the EVB with my STM32F691DISCOVERY board running the nanoFramework (COM5 on the hardware serial port pins D0,D1) I removed R17&R19.

After some tinkering, I found that R8 which is connected to the RAK811 module reset had to be cut as well for the shield to work on my Arduino Uno R3 and STM32F691DISCOVERY devices.

RAK811 EVB with R17,R19 & R8 cut

I can still run the Arduino Uno R3 and RAK811 EVB in the original configuration with a couple of jumper leads

RAK811 on Arduino with Serial connected to D10,D1 a SoftwareSerial port

For devices where I needed D10,D11 for a  Serial Peripheral Interface(SPI) I could use an FTDI board and a couple of other pins (in this case D2,D3) for serial logging.

RAK811 on Arduino with Serial connected to D2,D2 a SoftwareSerial port

After debugging some code I also replaced the small jumpers on P1 with a couple of jumper leads so it was less fiddly to swap from downloading to debugging.

RAK811 LPWAN EVB Part2

How can I use this…

Just over a week ago I purchased a RAK811 LPWAN Evaluation Board -AS923 and now I want to trial it with selection of devices and configurations.

Initially I didn’t want to modify the shield by removing resistors as I only have one, and I’m not certain what device(s) it will be used with. The initial hardware configuration required jumpers for the serial port, ground and 5V power.

Arduino Uno R3 and RAK811 LPWAN Evaluation board 5V config

After looking at the schematic it should be possible to use the shield with a 3v3 device.

RAK 811 EVB schematic pg1
RAK 811 EVB schematic pg2

I confirmed this with a Seeeduino V4.2 devices set to 3v3, by putting a jumper on J1 and shifting the jumper wire from the 5V to the 3V3 pin.

Seeeduino V4 and RAK811 LPWAN Evaluation board 3V3 config

The next step was to see how I could get the RAK shield working on other devices without jumpers. On Arduino Uno R3 devices D0&D1 are the hardware(HW) serial port which are used for uploading sketches, and diagnostic logging.

The shield also connects the module serial port to D0&D1 to D10&D11, so by removing R17&R19 the shield should work on a device This would also allow the use of the Serial Peripheral Interface(SPI) port for other applications.

Using the HW Serial port but without any logging.

Unplugging the jumpers to upload was painful but the lack of logging made it really hard to debug my code.

To get around this I configured a SoftwareSerial port on D2&D3 for logging.

/********************************************************
 * This demo is only supported after RUI firmware version 3.0.0.13.X on RAK811
 * Master Board Uart Receive buffer size at least 128 bytes. 
 ********************************************************/
//#define SERIAL_BUFFER_SIZE 128
//#define SERIAL_TX_BUFFER_SIZE 64
//#define SERIAL_RX_BUFFER_SIZE 128
//#define _SS_MAX_RX_BUFF 128
#include "RAK811.h"
#include "SoftwareSerial.h"
#define WORK_MODE LoRaWAN   //  LoRaWAN or LoRaP2P
#define JOIN_MODE OTAA    //  OTAA or ABP
#if JOIN_MODE == OTAA
String DevEui = "..."; // From TTN
String AppEui = "...";
String AppKey = "...";
#else JOIN_MODE == ABP
String NwkSKey = "...";
String AppSKey = "...";
String DevAddr = "...";
#endif

#define TXpin 3   // Set the virtual serial port pins
#define RXpin 2

SoftwareSerial DebugSerial(RXpin,TXpin); // Declare a virtual serial port for debugging
#define ATSerial Serial

char buffer[]= "48656C6C6F20776F726C6435";

bool InitLoRaWAN(void);
RAK811 RAKLoRa(ATSerial,DebugSerial);

void setup() {
  DebugSerial.begin(19200);
  DebugSerial.println(F("Starting"));
  while(DebugSerial.available())
  {
    DebugSerial.read(); 
  }
  
  ATSerial.begin(9600); //set ATSerial baudrate:This baud rate has to be consistent with  the baud rate of the WisNode device.
  while(ATSerial.available())
  {
    ATSerial.read(); 
  }

  if(!RAKLoRa.rk_setWorkingMode(0))  //set WisNode work_mode to LoRaWAN.
  {
    DebugSerial.println(F("set work_mode failed, please reset module."));
    while(1);
  }
  
  RAKLoRa.rk_getVersion();  //get RAK811 firmware version
  DebugSerial.println(RAKLoRa.rk_recvData());  //print version number

  DebugSerial.println(F("Start init RAK811 parameters..."));
 
  if (!InitLoRaWAN())  //init LoRaWAN
  {
    DebugSerial.println(F("Init error,please reset module.")); 
    while(1);
  }

  DebugSerial.println(F("Start to join LoRaWAN..."));
  while(!RAKLoRa.rk_joinLoRaNetwork(60))  //Joining LoRaNetwork timeout 60s
  {
    DebugSerial.println();
    DebugSerial.println(F("Rejoin again after 5s..."));
    delay(5000);
  }
  DebugSerial.println(F("Join LoRaWAN success"));

  if(!RAKLoRa.rk_isConfirm(0))  //set LoRa data send package type:0->unconfirm, 1->confirm
  {
    DebugSerial.println(F("LoRa data send package set error,please reset module.")); 
    while(1);    
  }
}

bool InitLoRaWAN(void)
{
  if(RAKLoRa.rk_setJoinMode(JOIN_MODE))  //set join_mode:OTAA
  {
    if(RAKLoRa.rk_setRegion(0))  //set region EU868
    {
      if (RAKLoRa.rk_initOTAA(DevEui, AppEui, AppKey))
      {
        DebugSerial.println(F("RAK811 init OK!"));  
        return true;    
      }
    }
  }
  return false;
}

void loop() 
{
  DebugSerial.println(F("Start send data..."));
  if (RAKLoRa.rk_sendData(1, buffer))
  {    
    //for (unsigned long start = millis(); millis() - start < 300000L;)
    for (unsigned long start = millis(); millis() - start < 10000L;)
    {
      String ret = RAKLoRa.rk_recvData();
      if(ret != NULL)
      { 
        DebugSerial.println("ret != NULL");
        DebugSerial.println(ret);
      }
      if((ret.indexOf("OK")>0)||(ret.indexOf("ERROR")>0))
      {
        DebugSerial.println(F("Go to Sleep."));
        RAKLoRa.rk_sleep(1);  //Set RAK811 enter sleep mode
        delay(10000);  //delay 10s
        RAKLoRa.rk_sleep(0);  //Wakeup RAK811 from sleep mode
        break;
      }
    }
  }
}

I used an FTDI module I had lying around to connect the diagnostic logging serial port on the test rig to my development box.

Using the HW Serial port but with logging.

Now I only had to unplug the jumpers for D0&D1 and change ports in the Arduino IDE. One port for debugging the other for downloading.

Depending on the application I may remove R8 so I can manually reset the shield.