.NET Core 5 SX127X library Arduino Duplex

The arduino-LoRa library LoRaDuplex sample is the basis for the last in this series of posts. The LoRaDuplex sample implements a basic protocol for addressed messages. The message payload starts with the destination address(byte), source address(byte), message counter(byte), payload length(byte), and then the payload(array of bytes).

LoRaDuplex

The sample code has configuration settings for the local address and destination (address).

#include <SPI.h>              // include libraries
#include <LoRa.h>

const int csPin = 10;          // LoRa radio chip select
const int resetPin = 9;       // LoRa radio reset
const int irqPin = 2;         // change for your board; must be a hardware interrupt pin

String outgoing;              // outgoing message

byte msgCount = 0;            // count of outgoing messages
byte localAddress = 0xAA;     // address of this device
byte destination = 0x0;      // destination to send to
long lastSendTime = 0;        // last send time
int interval = 2000;          // interval between sends

void setup() {
  Serial.begin(9600);                   // initialize serial
  while (!Serial);

  Serial.println("LoRa Duplex");

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

  Serial.println("LoRa init succeeded.");
}

void loop() {
  if (millis() - lastSendTime > interval) {
    String message = "HeLoRa World!";   // send a message
    sendMessage(message);
    Serial.println("Sending " + message);
    lastSendTime = millis();            // timestamp the message
    interval = random(2000) + 29000;    // 2-3 seconds
  }

  // parse for a packet, and call onReceive with the result:
  onReceive(LoRa.parsePacket());
}

void sendMessage(String outgoing) {
  LoRa.beginPacket();                   // start packet
  LoRa.write(destination);              // add destination address
  LoRa.write(localAddress);             // add sender address
  LoRa.write(msgCount);                 // add message ID
  LoRa.write(outgoing.length());        // add payload length
  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:
  int recipient = LoRa.read();          // recipient address
  byte sender = LoRa.read();            // sender address
  byte incomingMsgId = LoRa.read();     // incoming msg ID
  byte incomingLength = LoRa.read();    // incoming msg length

  String incoming = "";

  while (LoRa.available()) {
    incoming += (char)LoRa.read();
  }

  if (incomingLength != incoming.length()) {   // check length for error
    Serial.println("error: message length does not match length");
    return;                             // skip rest of function
  }

  // if the recipient isn't this device or broadcast,
  if (recipient != localAddress && recipient != 0xFF) {
    Serial.println("This message is not for me.");
    return;                             // skip rest of function
  }

  // if message is for this device, or broadcast, print details:
  Serial.println("Received from: 0x" + String(sender, HEX));
  Serial.println("Sent to: 0x" + String(recipient, HEX));
  Serial.println("Message ID: " + String(incomingMsgId));
  Serial.println("Message length: " + String(incomingLength));
  Serial.println("Message: " + incoming);
  Serial.println("RSSI: " + String(LoRa.packetRssi()));
  Serial.println("Snr: " + String(LoRa.packetSnr()));
  Serial.println();
}
Arduino Monitor displaying information about the messages sent and received by the Duplex sample

In the Visual Studio output window I could see the SX127XLoRaDeviceClient sending and receiving messages

Loaded '/usr/lib/dotnet/shared/Microsoft.NETCore.App/5.0.4/Microsoft.Win32.Primitives.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
17:26:10-TX to 0xAA from 0x00 count 1 length 28 "Hello LoRa from .NET Core! 1"
17:26:10-TX Done
17:26:20-TX to 0xAA from 0x00 count 2 length 28 "Hello LoRa from .NET Core! 2"
17:26:20-TX Done
17:26:30-TX to 0xAA from 0x00 count 3 length 28 "Hello LoRa from .NET Core! 3"
17:26:30-TX Done
17:26:31-RX to 0x00 from 0xAA count 0 length 13 "HeLoRa World!" snr 9.5 packet rssi -57dBm rssi -100dBm 
17:26:40-TX to 0xAA from 0x00 count 4 length 28 "Hello LoRa from .NET Core! 4"
17:26:40-TX Done
17:26:50-TX to 0xAA from 0x00 count 5 length 28 "Hello LoRa from .NET Core! 5"
17:26:50-TX Done
17:27:00-TX to 0xAA from 0x00 count 6 length 28 "Hello LoRa from .NET Core! 6"
17:27:00-TX Done
17:27:01-RX to 0x00 from 0xBB count 1 length 13 "HeLoRa World!" snr 9.8 packet rssi -50dBm rssi -100dBm 
17:27:10-TX to 0xAA from 0x00 count 7 length 28 "Hello LoRa from .NET Core! 7"
17:27:10-TX Done
17:27:20-TX to 0xAA from 0x00 count 8 length 28 "Hello LoRa from .NET Core! 8"
17:27:20-TX Done
17:27:30-TX to 0xAA from 0x00 count 9 length 28 "Hello LoRa from .NET Core! 9"
17:27:30-TX Done

I modified the SX127X.NetCore SX127XLoRaDeviceClient adding one final conditional compile option(LoRaDuplex) for this sample

static void Main(string[] args)
{
	int messageCount = 1;

	sX127XDevice.Initialise(
			SX127XDevice.RegOpModeMode.ReceiveContinuous,
			915000000.0,
			powerAmplifier: SX127XDevice.PowerAmplifier.PABoost,
#if LORA_SENDER // From the Arduino point of view
			rxDoneignoreIfCrcMissing: false
#endif
#if LORA_RECEIVER // From the Arduino point of view, don't actually need this as already inverted
			invertIQTX: true
#endif

#if LORA_SET_SYNCWORD
			syncWord: 0xF3,
			invertIQTX: true,
			rxDoneignoreIfCrcMissing: false
#endif
#if LORA_SET_SPREAD
			spreadingFactor: SX127XDevice.RegModemConfig2SpreadingFactor._256ChipsPerSymbol,
			invertIQTX: true,
			rxDoneignoreIfCrcMissing: false
#endif
#if LORA_SIMPLE_NODE // From the Arduino point of view
			invertIQTX: false,
			rxDoneignoreIfCrcMissing: false
#endif
#if LORA_SIMPLE_GATEWAY // From the Arduino point of view
			invertIQRX: true,
			rxDoneignoreIfCrcMissing: false
#endif
#if LORA_DUPLEX
			rxPayloadCrcOn: true
#endif
			);

#if DEBUG
	sX127XDevice.RegisterDump();
#endif

#if !LORA_RECEIVER
	sX127XDevice.OnReceive += SX127XDevice_OnReceive;
	sX127XDevice.Receive();
#endif
#if !LORA_SENDER
	sX127XDevice.OnTransmit += SX127XDevice_OnTransmit;
#endif

#if LORA_SENDER
	Thread.Sleep(-1);
#else
	Thread.Sleep(5000);
#endif

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

#if LORA_DUPLEX
		byte[] messageBytes = new byte[messageText.Length+4];

		messageBytes[0] = 0xaa;
		messageBytes[1] = 0x00;
		messageBytes[2] = (byte)messageCount;
		messageBytes[3] = (byte)messageText.Length;

		Array.Copy(UTF8Encoding.UTF8.GetBytes(messageText), 0, messageBytes, 4, messageBytes[3]);

		Console.WriteLine($"{DateTime.Now:HH:mm:ss}-TX to 0x{messageBytes[0]:X2} from 0x{messageBytes[1]:X2} count {messageBytes[2]} length {messageBytes[3]} \"{messageText}\"");
#else
		byte[] messageBytes = UTF8Encoding.UTF8.GetBytes(messageText);

		Console.WriteLine($"{DateTime.Now:HH:mm:ss}- Length {messageBytes.Length} \"{messageText}\"");
#endif

		messageCount += 1;

		sX127XDevice.Send(messageBytes);

		Thread.Sleep(10000);
	}
}

private static void SX127XDevice_OnReceive(object sender, SX127XDevice.OnDataReceivedEventArgs e)
{
	string messageText;

#if LORA_DUPLEX
	if ((e.Data[0] != 0x00) && (e.Data[0] != 0xFF))
	{
#if DEBUG
		Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss}-RX to 0x{e.Data[0]:X2} from 0x{e.Data[1]:X2} invalid address");
#endif
		return;
	}

	// check payload not to long/short
	if  ((e.Data[3] + 4) != e.Data.Length)
	{
		Console.WriteLine($"{DateTime.UtcNow:hh:mm:ss}-RX Invalid payload");

		return;
	}

	try
	{
		messageText = UTF8Encoding.UTF8.GetString(e.Data, 4, e.Data[3]);

		Console.WriteLine($"{DateTime.Now:HH:mm:ss}-RX to 0x{e.Data[0]:X2} from 0x{e.Data[1]:X2} count {e.Data[2]} length {e.Data[3]} \"{messageText}\" snr {e.PacketSnr:0.0} packet rssi {e.PacketRssi}dBm rssi {e.Rssi}dBm ");
	}
	catch (Exception ex)
	{
		Console.WriteLine(ex.Message);
	}
#else
	try
	{
		messageText = UTF8Encoding.UTF8.GetString(e.Data);

		Console.WriteLine($"{DateTime.Now:HH:mm:ss}-RX length {e.Data.Length} \"{messageText}\" snr {e.PacketSnr:0.0} packet rssi {e.PacketRssi}dBm rssi {e.Rssi}dBm ");
	}
	catch (Exception ex)
	{
		Console.WriteLine(ex.Message);
	}
#endif
}

The inbound messages have to have a valid Cyclic Redundancy Check(CRC) and I ignore messages with an invalid payload length. The message protocol is insecure (but fine for demos) as the messages are sent as “plain text”, and the message headers/payload can be tampered with.

Summary

While testing the LoRaDuplex sample I found a problem with how my code managed the invertIQRX & invertIQTX flags in RegInvertIQ. I noticed the even though I was setting the InvertIQRX(bit6) and invertIQTX(bit0) flags correctly messages weren’t getting delivered.

Semtech SX127X data sheet RegInvertQ and RegInvertQ2 documetnation

After looking at my code I realised I wasn’t configuring the RegInvertIQ properly because bits 1-5 were getting set to 0x0 (initially I had byte regInvertIQValue = 0) rather than 0x13(regInvertIQValue = RegInvertIdDefault)

...
// RegInvertId
private const byte RegInvertIdDefault = 0b00100110;
private const byte InvertIqRXOn = 0b01000000;
private const byte InvertIqRXOff = 0b00000000;
public const bool InvertIqRXDefault = false;

private const byte InvertIqTXOn =  0b00000001; 
private const byte InvertIqTXOff = 0b00000000;
...

if ((invertIQRX != InvertIqRXDefault) || (invertIQTX != InvertIqTXDefault))
{
	// Initially this was byte regInvertIQValue = 0;
	byte regInvertIQValue = RegInvertIdDefault;

	if (invertIQRX)
	{
		regInvertIQValue |= InvertIqRXOn;
	}

	if (invertIQTX)
	{
		regInvertIQValue |= InvertIqTXOn;
	}

	this.WriteByte((byte)Registers.RegInvertIQ, regInvertIQValue);

	if (invertIQRX || invertIQTX)
	{
		this.WriteByte((byte)Registers.RegInvertIQ2, RegInvertIq2On);
	}
	else
	{
		this.WriteByte((byte)Registers.RegInvertIQ2, RegInvertIq2Off);
	}
}

SX127X InvertIQ RX & InvertIQ TX

My code was wrong, but now it’s less wrong

When I first fired up the my Seeeduino V4.2 + Dragino LoRa Shield using an application based on the Arduino-LoRa LoRaSimpleNode it didn’t work until in my library I set the sixth bit of RegInvertIQ to true.

Arduino monitor showing register dump just after startup

The first issue was my RegInvertIQ register configuration code was wrong, it looks like I copied ‘n’ paste the code and forgot fix it up.

// RegDetectOptimize
if (detectionOptimize != RegDetectOptimizeDectionOptimizeDefault)
{
	this.WriteByte((byte)Registers.RegDetectOptimize, (byte)detectionOptimize);
}

// RegInvertIQ
if (invertIQ != false)
{
	this.WriteByte((byte)Registers.RegInvertIQ, (byte)detectionThreshold);
}

// RegSyncWordDefault 
if (syncWord != RegSyncWordDefault)
{
	this.WriteByte((byte)Registers.RegSyncWord, syncWord);
}

Even when I fixed up the code something still wasn’t quite right so I had a closer look at the SX127X datasheet and the LoRa-Arduino library code.

Semtech SX127X Datasheet RegInvertIQ information for TX and RX settings
void LoRaClass::enableCrc()
{
  writeRegister(REG_MODEM_CONFIG_2, readRegister(REG_MODEM_CONFIG_2) | 0x04);
}

void LoRaClass::disableCrc()
{
  writeRegister(REG_MODEM_CONFIG_2, readRegister(REG_MODEM_CONFIG_2) & 0xfb);
}

void LoRaClass::enableInvertIQ()
{
  writeRegister(REG_INVERTIQ,  0x66);
  writeRegister(REG_INVERTIQ2, 0x19);
}

void LoRaClass::disableInvertIQ()
{
  writeRegister(REG_INVERTIQ,  0x27);
  writeRegister(REG_INVERTIQ2, 0x1d);
}

void LoRaClass::setOCP(uint8_t mA)
{
  uint8_t ocpTrim = 27;

  if (mA <= 120) {
    ocpTrim = (mA - 45) / 5;
  } else if (mA <=240) {
    ocpTrim = (mA + 30) / 10;
  }

In RegInvertIQ the sixth bit is the RX flag and the zeroth bit is the TX flag, in the enable method the zeroth bit was not set(even number) and in the disable method it was set (odd number). The enable and disable method were “inverting” both the TX and RX lines.

When I did a RegisterDump the initial value of RegInvertIQ was 0x27 which is a bit odd as datasheet indicated the default value of the zeroth bit is 0.

To double check I inspected the source of a couple of libraries including the ARM Lora-net/sx126x_driver: Driver for SX126x radio (github.com)

/*!
 * RegDetectOptimize
 */
#define RFLR_DETECTIONOPTIMIZE_MASK                 0xF8
#define RFLR_DETECTIONOPTIMIZE_SF7_TO_SF12          0x03 // Default
#define RFLR_DETECTIONOPTIMIZE_SF6                  0x05

/*!
 * RegInvertIQ
 */
#define RFLR_INVERTIQ_RX_MASK                       0xBF
#define RFLR_INVERTIQ_RX_OFF                        0x00
#define RFLR_INVERTIQ_RX_ON                         0x40
#define RFLR_INVERTIQ_TX_MASK                       0xFE
#define RFLR_INVERTIQ_TX_OFF                        0x01
#define RFLR_INVERTIQ_TX_ON                         0x00

/*!
 * RegDetectionThreshold
 */
#define RFLR_DETECTIONTHRESH_SF7_TO_SF12            0x0A // Default
#define RFLR_DETECTIONTHRESH_SF6                    0x0C

/*!
 * RegInvertIQ2
 */
#define RFLR_INVERTIQ2_ON                           0x19
#define RFLR_INVERTIQ2_OFF                          0x1D

/*!
 * RegDioMapping1
 */
case MODEM_LORA:
   if (_rf_settings.lora.iq_inverted == true) {
       write_to_register(REG_LR_INVERTIQ, ((read_register(REG_LR_INVERTIQ)
                                            & RFLR_INVERTIQ_TX_MASK & RFLR_INVERTIQ_RX_MASK)
                                          | RFLR_INVERTIQ_RX_ON | RFLR_INVERTIQ_TX_OFF));
       write_to_register(REG_LR_INVERTIQ2, RFLR_INVERTIQ2_ON);
   } else {
       write_to_register(REG_LR_INVERTIQ, ((read_register(REG_LR_INVERTIQ)
                                            & RFLR_INVERTIQ_TX_MASK & RFLR_INVERTIQ_RX_MASK)
                                           | RFLR_INVERTIQ_RX_OFF | RFLR_INVERTIQ_TX_OFF));
       write_to_register(REG_LR_INVERTIQ2, RFLR_INVERTIQ2_OFF);
   }

Summary

My code supports the toggling of the RegInvertIQ “InvertIQ RX” and “InvertIQ TX” flags independently so users of the SX127X.NetCore library will need to pay attention to the configuration settings of the libraries used on other client devices.