Random wanderings through Microsoft Azure esp. PaaS plumbing, the IoT bits, AI on Micro controllers, AI on Edge Devices, .NET nanoFramework, .NET Core on *nix and ML.NET+ONNX
One afternoon the issue occurred several times in a row, the application wouldn’t startup because the SX127X device detection failed and message transmission was also not being confirmed.(TX Done).
Visual Studio output windows with SX127X detection failure
Visual Studio output windows with no Transmit confirmations
public SX127XDevice(SpiDevice spiDevice, GpioController gpioController, int interruptPin, int resetPin)
{
_gpioController = gpioController;
// Factory reset pin configuration
_resetPin = resetPin;
_gpioController.OpenPin(resetPin, PinMode.Output);
_gpioController.Write(resetPin, PinValue.Low);
Thread.Sleep(20);
_gpioController.Write(resetPin, PinValue.High);
Thread.Sleep(100);
_registerManager = new RegisterManager(spiDevice, RegisterAddressReadMask, RegisterAddressWriteMask);
// Once the pins setup check that SX127X chip is present
Byte regVersionValue = _registerManager.ReadByte((byte)Configuration.Registers.RegVersion);
if (regVersionValue != Configuration.RegVersionValueExpected)
{
throw new ApplicationException("Semtech SX127X not found");
}
// Interrupt pin for RX message & TX done notification
_gpioController.OpenPin(interruptPin, PinMode.InputPullDown);
_gpioController.RegisterCallbackForPinValueChangedEvent(interruptPin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
}
I could single step through the code and inspect variables with the debugger and it looks like a timing issue with order of the strobing of the reset pin and the initialisation of the RegisterManager. I’ll spend and hour starting and stopping the application, then smoke test the code for 24 hours with a couple of other devices generating traffic just to check.
The TransmitInterrupt application loads the message to be sent into the First In First Out(FIFO) buffer, RegDioMapping1 is set to interrupt onTxDone(PacketSent-00), then RegRegOpMode-Mode is set to Transmit. When the message has been sent InterruptGpioPin_ValueChanged is called, and the TxDone(0b00001000) flag is set in the RegIrqFlags register.
The ReceiveInterrupt application sets the RegDioMapping1 to interrupt on RxDone(PacketReady-00), then the RegRegOpMode-Mode is set to Receive(TX-101). When a message is received InterruptGpioPin_ValueChanged is called, with the RxDone(0b00001000) flag set in the RegIrqFlags register, and then the message is read from First In First Out(FIFO) buffer.
namespace devMobile.IoT.SX127x.ReceiveTransmitInterrupt
{
...
public sealed class SX127XDevice
{
...
public SX127XDevice(int busId, int chipSelectLine, int interruptPin, int resetPin)
{
var settings = new SpiConnectionSettings(busId, chipSelectLine)
{
ClockFrequency = 1000000,
Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
SharingMode = SpiSharingMode.Shared
};
SX127XTransceiver = new SpiDevice(settings);
GpioController gpioController = new GpioController();
// Factory reset pin configuration
gpioController.OpenPin(resetPin, PinMode.Output);
gpioController.Write(resetPin, PinValue.Low);
Thread.Sleep(20);
gpioController.Write(resetPin, PinValue.High);
Thread.Sleep(20);
// Interrupt pin for RX message & TX done notification
gpioController.OpenPin(interruptPin, PinMode.InputPullDown);
gpioController.RegisterCallbackForPinValueChangedEvent(interruptPin, PinEventTypes.Rising, InterruptGpioPin_ValueChanged);
}
...
}
private void InterruptGpioPin_ValueChanged(object sender, PinValueChangedEventArgs e)
{
byte irqFlags = this.ReadByte(0x12); // RegIrqFlags
Debug.WriteLine($"RegIrqFlags 0X{irqFlags:x2}");
if ((irqFlags & 0b01000000) == 0b01000000) // RxDone
{
Debug.WriteLine("Receive-Message");
byte currentFifoAddress = this.ReadByte(0x10); // RegFifiRxCurrent
this.WriteByte(0x0d, currentFifoAddress); // RegFifoAddrPtr
byte numberOfBytes = this.ReadByte(0x13); // RegRxNbBytes
// Allocate buffer for message
byte[] messageBytes = this.ReadBytes(0X0, numberOfBytes);
// Remove unprintable characters from messages
for (int index = 0; index < messageBytes.Length; index++)
{
if ((messageBytes[index] < 0x20) || (messageBytes[index] > 0x7E))
{
messageBytes[index] = 0x20;
}
}
string messageText = UTF8Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length);
Debug.WriteLine($"Received {messageBytes.Length} byte message {messageText}");
}
if ((irqFlags & 0b00001000) == 0b00001000) // TxDone
{
this.WriteByte(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous
Debug.WriteLine("Transmit-Done");
}
this.WriteByte(0x40, 0b00000000); // RegDioMapping1 0b00000000 DI0 RxReady & TxReady
this.WriteByte(0x12, 0xff);// RegIrqFlags
}
public class Program
{
...
#if NETDUINO3_WIFI
private const int SpiBusId = 2;
#endif
...
public static void Main()
{
int SendCount = 0;
...
#if NETDUINO3_WIFI
// Arduino D10->PB10
int chipSelectLine = PinNumber('B', 10);
// Arduino D9->PE5
int resetPinNumber = PinNumber('E', 5);
// Arduino D2 -PA3
int interruptPinNumber = PinNumber('A', 3);
#endif
...
Debug.WriteLine("devMobile.IoT.SX127x.ReceiveTransmitInterrupt starting");
try
{
...
#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
SX127XDevice sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine, interruptPinNumber, resetPinNumber);
#endif
Thread.Sleep(500);
// Put device into LoRa + Sleep mode
sx127XDevice.WriteByte(0x01, 0b10000000); // RegOpMode
// Set the frequency to 915MHz
byte[] frequencyWriteBytes = { 0xE4, 0xC0, 0x00 }; // RegFrMsb, RegFrMid, RegFrLsb
sx127XDevice.WriteBytes(0x06, frequencyWriteBytes);
// More power PA Boost
sx127XDevice.WriteByte(0x09, 0b10000000); // RegPaConfig
sx127XDevice.WriteByte(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous
while (true)
{
// Set the Register Fifo address pointer
sx127XDevice.WriteByte(0x0E, 0x00); // RegFifoTxBaseAddress
// Set the Register Fifo address pointer
sx127XDevice.WriteByte(0x0D, 0x0); // RegFifoAddrPtr
string messageText = $"Hello LoRa {SendCount += 1}!";
// load the message into the fifo
byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
sx127XDevice.WriteBytes(0x0, messageBytes); // RegFifo
// Set the length of the message in the fifo
sx127XDevice.WriteByte(0x22, (byte)messageBytes.Length); // RegPayloadLength
sx127XDevice.WriteByte(0x40, 0b01000000); // RegDioMapping1 0b00000000 DI0 RxReady & TxReady
sx127XDevice.WriteByte(0x01, 0b10000011); // RegOpMode
Debug.WriteLine($"Sending {messageBytes.Length} bytes message {messageText}");
Thread.Sleep(10000);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
...
}
}
ReceiveTransmitInterrupt application output
The ReceiveTransmitInterrupt application combines the functionality TransmitInterrupt and ReceiveInterrupt programs. The key differences are the RegDioMapping1 setup and in InterruptGpioPin_ValueChanged where the TxDone & RxDone flags in the RegIrqFlags register specify how the interrupt is handled.
For testing nanoFramework device transmit and receive functionality I used an Arduino/Seeeduino with a Dragino LoRa Shield (running one of the Arduino-LoRa samples) as a client device. This was so I could “bootstrap” connectivity and test interoperability with other libraries/platforms.
Arduino/Netduino devices for .NET nanoFramework interoperability test-rig
I started with transmit as I was confident my Seeeduino + Dragino LoRa Shield could receive messages. The TransmitBasic application puts the device into LoRa + Sleep mode as after reset/powering up the device is in FSK/OOK, Low Frequency + Standby mode).
SX127X RegOpMode options
After loading the message to be sent into the First In First Out(FIFO) buffer, the RegOpMode-Mode is set to Transmit(TX-011), and then the RegIrqFlags register is polled until the TxDone flag is set.
SX127X ReqIrqFlags options
public static void Main()
{
int SendCount = 0;
...
Debug.WriteLine("devMobile.IoT.SX127x.TransmitBasic starting");
try
{
...
#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
SX127XDevice sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine, resetPinNumber);
#endif
Thread.Sleep(500);
// Put device into LoRa + Standby mode
sx127XDevice.WriteByte(0x01, 0b10000000); // RegOpMode
// Set the frequency to 915MHz
byte[] frequencyBytes = { 0xE4, 0xC0, 0x00 }; // RegFrMsb, RegFrMid, RegFrLsb
sx127XDevice.WriteBytes(0x06, frequencyBytes);
// More power PA Boost
sx127XDevice.WriteByte(0x09, 0b10000000); // RegPaConfig
sx127XDevice.RegisterDump();
while (true)
{
sx127XDevice.WriteByte(0x0E, 0x0); // RegFifoTxBaseAddress
// Set the Register Fifo address pointer
sx127XDevice.WriteByte(0x0D, 0x0); // RegFifoAddrPtr
string messageText = $"Hello LoRa from .NET nanoFramework {SendCount += 1}!";
// load the message into the fifo
byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);
sx127XDevice.WriteBytes(0x0, messageBytes); // RegFifo
// Set the length of the message in the fifo
sx127XDevice.WriteByte(0x22, (byte)messageBytes.Length); // RegPayloadLength
Debug.WriteLine($"Sending {messageBytes.Length} bytes message {messageText}");
// Set the mode to LoRa + Transmit
sx127XDevice.WriteByte(0x01, 0b10000011); // RegOpMode
// Wait until send done, no timeouts in PoC
Debug.WriteLine("Send-wait");
byte irqFlags = sx127XDevice.ReadByte(0x12); // RegIrqFlags
while ((irqFlags & 0b00001000) == 0) // wait until TxDone cleared
{
Thread.Sleep(10);
irqFlags = sx127XDevice.ReadByte(0x12); // RegIrqFlags
Debug.Write(".");
}
Debug.WriteLine("");
sx127XDevice.WriteByte(0x12, 0b00001000); // clear TxDone bit
Debug.WriteLine("Send-Done");
Thread.Sleep(30000);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
}
Transmit Basic application output
Once the TransmitBasic application was sending messages reliably I started working on the ReceiveBasic application. As the ReceiveBasic application starts up the SX127X RegOpMode has to be set to sleep/standby so the device can be configured. TOnce that is completed RegOpMode-Mode is set to RxContinuous(101), and the RegIrqFlags register is polled until the RxDone flag is set.
public static void Main()
{
...
Debug.WriteLine("devMobile.IoT.SX127x.ReceiveBasic starting");
try
{
...
#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
SX127XDevice sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine, resetPinNumber);
#endif
Thread.Sleep(500);
// Put device into LoRa + Sleep mode
sx127XDevice.WriteByte(0x01, 0b10000000); // RegOpMode
// Set the frequency to 915MHz
byte[] frequencyBytes = { 0xE4, 0xC0, 0x00 }; // RegFrMsb, RegFrMid, RegFrLsb
sx127XDevice.WriteBytes(0x06, frequencyBytes);
sx127XDevice.WriteByte(0x0F, 0x0); // RegFifoRxBaseAddress
sx127XDevice.WriteByte(0x01, 0b10000101); // RegOpMode set LoRa & RxContinuous
while (true)
{
// Wait until a packet is received, no timeouts in PoC
Debug.WriteLine("Receive-Wait");
byte irqFlags = sx127XDevice.ReadByte(0x12); // RegIrqFlags
while ((irqFlags & 0b01000000) == 0) // wait until RxDone cleared
{
Thread.Sleep(100);
irqFlags = sx127XDevice.ReadByte(0x12); // RegIrqFlags
Debug.Write(".");
}
Debug.WriteLine("");
Debug.WriteLine($"RegIrqFlags 0X{irqFlags:X2}");
Debug.WriteLine("Receive-Message");
byte currentFifoAddress = sx127XDevice.ReadByte(0x10); // RegFifiRxCurrent
sx127XDevice.WriteByte(0x0d, currentFifoAddress); // RegFifoAddrPtr
byte numberOfBytes = sx127XDevice.ReadByte(0x13); // RegRxNbBytes
// Read the message from the FIFO
byte[] messageBytes = sx127XDevice.ReadBytes(0x00, numberOfBytes);
sx127XDevice.WriteByte(0x0d, 0);
sx127XDevice.WriteByte(0x12, 0b11111111); // RegIrqFlags clear all the bits
// Remove unprintable characters from messages
for (int index = 0; index < messageBytes.Length; index++)
{
if ((messageBytes[index] < 0x20) || (messageBytes[index] > 0x7E))
{
messageBytes[index] = 0x20;
}
}
string messageText = UTF8Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length);
Debug.WriteLine($"Received {messageBytes.Length} byte message {messageText}");
Debug.WriteLine("Receive-Done");
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
Receive Basic application output
Every so often the ReceiveBasic application would display a message sent on the same frequency by a device somewhere nearby.
ReceiveBasic application messages from unknown source
I need to do some more investigation into whether writing 0b00001000 (Transmit) vs. 0b11111111(Receive) to RegIrqFlags is important.
Now that I could reliably dump all the Dragino shield registers I wanted to be able to configure the Semtech 127X device and reset it back to factory settings. A factory reset is done by strobing the SX127X reset pin.
SX127X Reset timing diagram
SX127X Reset process
To support this I added a constructor with an additional parameter for the reset General Purpose Input Output(GPIO) pin number to the SX127XDevice class. The original constructor was retained as the SX127X reset pin is not connected on the SparkFun LoRa Gateway-1-Channel (ESP32) and a limited number of other devices.
namespace devMobile.IoT.SX127x.RegisterReadAndWrite
{
using System;
using System.Diagnostics;
using System.Threading;
using System.Device.Gpio;
using System.Device.Spi;
#if ESP32_WROOM_32_LORA_1_CHANNEL
using nanoFramework.Hardware.Esp32;
#endif
public sealed class SX127XDevice
{
private const byte RegisterAddressMinimum = 0X0;
private const byte RegisterAddressMaximum = 0x42;
private const byte RegisterAddressReadMask = 0X7f;
private const byte RegisterAddressWriteMask = 0x80;
private readonly SpiDevice SX127XTransceiver;
public SX127XDevice(int busId, int chipSelectLine, int resetPin)
{
var settings = new SpiConnectionSettings(busId, chipSelectLine)
{
ClockFrequency = 1000000,
Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
SharingMode = SpiSharingMode.Shared
};
SX127XTransceiver = new SpiDevice(settings);
// Factory reset pin configuration
GpioController gpioController = new GpioController();
gpioController.OpenPin(resetPin, PinMode.Output);
gpioController.Write(resetPin, PinValue.Low);
Thread.Sleep(20);
gpioController.Write(resetPin, PinValue.High);
Thread.Sleep(20);
}
public SX127XDevice(int busId, int chipSelectLine)
{
var settings = new SpiConnectionSettings(busId, chipSelectLine)
{
ClockFrequency = 1000000,
Mode = SpiMode.Mode0,// From SemTech docs pg 80 CPOL=0, CPHA=0
SharingMode = SpiSharingMode.Shared,
};
SX127XTransceiver = new SpiDevice(settings);
}
public Byte ReadByte(byte registerAddress)
{
byte[] writeBuffer = new byte[] { registerAddress &= RegisterAddressReadMask, 0x0 };
byte[] readBuffer = new byte[writeBuffer.Length];
SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);
return readBuffer[1];
}
public ushort ReadWord(byte address)
{
byte[] writeBuffer = new byte[] { address &= RegisterAddressReadMask, 0x0, 0x0 };
byte[] readBuffer = new byte[writeBuffer.Length];
SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);
return (ushort)(readBuffer[2] + (readBuffer[1] << 8));
}
public ushort ReadWordMsbLsb(byte address)
{
byte[] writeBuffer = new byte[] { address &= RegisterAddressReadMask, 0x0, 0x0 };
byte[] readBuffer = new byte[writeBuffer.Length];
SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);
return (ushort)((readBuffer[1] << 8) + readBuffer[2]);
}
public byte[] ReadBytes(byte address, byte length)
{
byte[] writeBuffer = new byte[length + 1];
byte[] readBuffer = new byte[writeBuffer.Length];
byte[] replyBuffer = new byte[length];
writeBuffer[0] = address &= RegisterAddressReadMask;
SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);
Array.Copy(readBuffer, 1, replyBuffer, 0, length);
return replyBuffer;
}
public void WriteByte(byte address, byte value)
{
byte[] writeBuffer = new byte[] { address |= RegisterAddressWriteMask, value };
byte[] readBuffer = new byte[writeBuffer.Length];
SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);
}
public void WriteWord(byte address, ushort value)
{
byte[] valueBytes = BitConverter.GetBytes(value);
byte[] writeBuffer = new byte[] { address |= RegisterAddressWriteMask, valueBytes[0], valueBytes[1] };
byte[] readBuffer = new byte[writeBuffer.Length];
SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);
}
public void WriteWordMsbLsb(byte address, ushort value)
{
byte[] valueBytes = BitConverter.GetBytes(value);
byte[] writeBuffer = new byte[] { address |= RegisterAddressWriteMask, valueBytes[1], valueBytes[0] };
byte[] readBuffer = new byte[writeBuffer.Length];
SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);
}
public void WriteBytes(byte address, byte[] bytes)
{
byte[] writeBuffer = new byte[1 + bytes.Length];
byte[] readBuffer = new byte[writeBuffer.Length];
Array.Copy(bytes, 0, writeBuffer, 1, bytes.Length);
writeBuffer[0] = address |= RegisterAddressWriteMask;
SX127XTransceiver.TransferFullDuplex(writeBuffer, readBuffer);
}
public void RegisterDump()
{
Debug.WriteLine("Register dump");
for (byte registerIndex = RegisterAddressMinimum; registerIndex <= RegisterAddressMaximum; registerIndex++)
{
byte registerValue = this.ReadByte(registerIndex);
Debug.WriteLine($"Register 0x{registerIndex:x2} - Value 0X{registerValue:x2}");
}
Debug.WriteLine("");
}
}
public class Program
{
#if ESP32_WROOM_32_LORA_1_CHANNEL
private const int SpiBusId = 1;
#endif
#if NETDUINO3_WIFI
private const int SpiBusId = 2;
#endif
#if ST_STM32F769I_DISCOVERY
private const int SpiBusId = 2;
#endif
public static void Main()
{
byte[] frequencyBytes;
#if ESP32_WROOM_32_LORA_1_CHANNEL // No reset line for this device as it isn't connected on SX127X
int chipSelectLine = Gpio.IO16;
#endif
#if NETDUINO3_WIFI
// Arduino D10->PB10
int chipSelectLine = PinNumber('B', 10);
// Arduino D9->PE5
int resetPinNumber = PinNumber('E', 5);
#endif
#if ST_STM32F769I_DISCOVERY
// Arduino D10->PA11
int chipSelectLine = PinNumber('A', 11);
// Arduino D9->PH6
int resetPinNumber = PinNumber('H', 6);
#endif
Debug.WriteLine("devMobile.IoT.SX127x.RegisterReadAndWrite starting");
try
{
#if ESP32_WROOM_32_LORA_1_CHANNEL
Configuration.SetPinFunction(Gpio.IO12, DeviceFunction.SPI1_MISO);
Configuration.SetPinFunction(Gpio.IO13, DeviceFunction.SPI1_MOSI);
Configuration.SetPinFunction(Gpio.IO14, DeviceFunction.SPI1_CLOCK);
SX127XDevice sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine);
#endif
#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
SX127XDevice sx127XDevice = new SX127XDevice(SpiBusId, chipSelectLine, resetPinNumber);
#endif
Thread.Sleep(500);
sx127XDevice.RegisterDump();
while (true)
{
Debug.WriteLine("Read RegOpMode (read byte)");
Byte regOpMode1 = sx127XDevice.ReadByte(0x1);
Debug.WriteLine($"RegOpMode 0x{regOpMode1:x2}");
Debug.WriteLine("Set LoRa mode and sleep mode (write byte)");
sx127XDevice.WriteByte(0x01, 0b10000000);
Debug.WriteLine("Read RegOpMode (read byte)");
Byte regOpMode2 = sx127XDevice.ReadByte(0x1);
Debug.WriteLine($"RegOpMode 0x{regOpMode2:x2}");
Debug.WriteLine("Read the preamble (read word)");
ushort preamble = sx127XDevice.ReadWord(0x20);
Debug.WriteLine($"Preamble 0x{preamble:x2}");
Console.WriteLine("Read the preamble (read word)"); // Should be 0x08
preamble = sx127XDevice.ReadWordMsbLsb(0x20);
Debug.WriteLine($"Preamble 0x{preamble:x2}");
Debug.WriteLine("Read the centre frequency (read byte array)");
frequencyBytes = sx127XDevice.ReadBytes(0x06, 3);
Debug.WriteLine($"Frequency Msb 0x{frequencyBytes[0]:x2} Mid 0x{frequencyBytes[1]:x2} Lsb 0x{frequencyBytes[2]:x2}");
Debug.WriteLine("Set the centre frequency to 915MHz (write byte array)");
byte[] frequencyWriteBytes = { 0xE4, 0xC0, 0x00 };
sx127XDevice.WriteBytes(0x06, frequencyWriteBytes);
Debug.WriteLine("Read the centre frequency (read byte array)");
frequencyBytes = sx127XDevice.ReadBytes(0x06, 3);
Debug.WriteLine($"Frequency Msb 0x{frequencyBytes[0]:x2} Mid 0x{frequencyBytes[1]:x2} Lsb 0x{frequencyBytes[2]:x2}");
sx127XDevice.RegisterDump();
Thread.Sleep(30000);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
#if NETDUINO3_WIFI || ST_STM32F769I_DISCOVERY
static int PinNumber(char port, byte pin)
{
if (port < 'A' || port > 'J')
throw new ArgumentException();
return ((port - 'A') * 16) + pin;
}
#endif
}
}
The PinNumber helper is more user friendly that the raw numbers and is “inspired” by sample .NET nanoFramework General Purpose Input Output(GPIO) sample code.
Each method was tested by read/writing suitable register(s) in the device configuration (Needed to set it into LoRa mode first).
The next step is to extract the Serial Peripheral Interface(SPI) register access functionality into a module and configure the bare minimum of settings required to get the SX127X to receive and transmit messages.
All this madness started because I wasn’t confident the frequency calculation of the Emmellsoft Dragino.Lora code was correct. Over the last couple of years I have also found bugs in my Transmit Power, InvertIQ RX/TX with many others yet to be discovered.
After building a nanoFramework library “inspired” by the RakWirelessArduinolibrary(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
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 beta and I’ll be testing and fixing bugs for the next couple of days.
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.