From aca1d78a9742e17947eaf5cddb63a10f10e3fafe Mon Sep 17 00:00:00 2001
From: Amalinda Gamage <5582466+jgamage91@users.noreply.github.com>
Date: Sun, 29 Oct 2023 21:19:00 +0800
Subject: [PATCH] added functionality for LoRa Alliance TR-13 Enabling CSMA for
 LoRaWAN (#859)

* added functionality for LoRa Alliance TR-13 Enabling CSMA for LoRaWAN

* Addressed feedback on CSMA implementation

* symbolNumValues[6] array no longer needed as we will utilize only two symbol CAD operations for all SFs.
---
 src/modules/SX126x/SX126x.cpp     | 43 ++++++++++++++------
 src/protocols/LoRaWAN/LoRaWAN.cpp | 66 ++++++++++++++++++++++++++++++-
 src/protocols/LoRaWAN/LoRaWAN.h   | 25 ++++++++++++
 3 files changed, 120 insertions(+), 14 deletions(-)

diff --git a/src/modules/SX126x/SX126x.cpp b/src/modules/SX126x/SX126x.cpp
index a312a011..7c6f8baa 100644
--- a/src/modules/SX126x/SX126x.cpp
+++ b/src/modules/SX126x/SX126x.cpp
@@ -1702,27 +1702,41 @@ int16_t SX126x::setRx(uint32_t timeout) {
   return(this->mod->SPIwriteStream(RADIOLIB_SX126X_CMD_SET_RX, data, 3, true, false));
 }
 
+ 
 int16_t SX126x::setCad(uint8_t symbolNum, uint8_t detPeak, uint8_t detMin) {
-  // default CAD parameters for assigned SF as per Semtech AN1200.48, Rev 2.1, Page 50
-  uint8_t detPeakValues[8] = { 22, 22, 22, 22, 23, 24, 25, 28};
-  uint8_t symbolNumValues[8] = { RADIOLIB_SX126X_CAD_ON_2_SYMB,
-                                 RADIOLIB_SX126X_CAD_ON_2_SYMB,
-                                 RADIOLIB_SX126X_CAD_ON_2_SYMB,
-                                 RADIOLIB_SX126X_CAD_ON_2_SYMB,
-                                 RADIOLIB_SX126X_CAD_ON_4_SYMB,
-                                 RADIOLIB_SX126X_CAD_ON_4_SYMB,
-                                 RADIOLIB_SX126X_CAD_ON_4_SYMB,
-                                 RADIOLIB_SX126X_CAD_ON_4_SYMB };
+  // default CAD parameters are shown in Semtech AN1200.48, page 41.
+  uint8_t detPeakValues[6] = { 22, 22, 24, 25, 26, 30};
+
+  // CAD parameters aren't available for SF-6. Just to be safe.
+  if(this->spreadingFactor < 7) {
+    this->spreadingFactor = 7;
+  } else if(this->spreadingFactor > 12) {
+    this->spreadingFactor = 12;
+  }
+
   // build the packet
   uint8_t data[7];
-  data[0] = symbolNumValues[this->spreadingFactor - 5];
-  data[1] = detPeakValues[this->spreadingFactor - 5];
+  data[0] = RADIOLIB_SX126X_CAD_ON_2_SYMB;
+  data[1] = detPeakValues[this->spreadingFactor - 7];
   data[2] = RADIOLIB_SX126X_CAD_PARAM_DET_MIN;
   data[3] = RADIOLIB_SX126X_CAD_GOTO_STDBY;
   data[4] = 0x00;
   data[5] = 0x00;
   data[6] = 0x00;
 
+
+ /*
+  CAD Configuration Note:
+  The default CAD configuration applied by `scanChannel` overrides the optimal SF-specific configurations, leading to suboptimal detection.
+  I.e., anything that is not RADIOLIB_SX126X_CAD_PARAM_DEFAULT is overridden. But CAD settings are SF specific.
+  To address this, the user override has been commented out, ensuring consistent application of the optimal CAD settings as 
+    per Semtech's Application Note AN1200.48 (page 41) for the 125KHz setting. This approach significantly reduces false CAD occurrences.
+    Testing has shown that there is no reason for a user to change CAD settings for anything other than most optimal ones described in AN1200.48 .
+  However, this change deos not respect CAD configs from the LoRaWAN layer. Future considerations or use cases might require revisiting this decision.
+  Hence this note.
+*/
+
+/*
   // set user-provided values
   if(symbolNum != RADIOLIB_SX126X_CAD_PARAM_DEFAULT) {
     data[0] = symbolNum;
@@ -1736,6 +1750,9 @@ int16_t SX126x::setCad(uint8_t symbolNum, uint8_t detPeak, uint8_t detMin) {
     data[2] = detMin;
   }
 
+*/
+
+
   // configure parameters
   int16_t state = this->mod->SPIwriteStream(RADIOLIB_SX126X_CMD_SET_CAD_PARAMS, data, 7);
   RADIOLIB_ASSERT(state);
@@ -2030,7 +2047,7 @@ int16_t SX126x::config(uint8_t modem) {
   state = this->mod->SPIwriteStream(RADIOLIB_SX126X_CMD_SET_RX_TX_FALLBACK_MODE, data, 1);
   RADIOLIB_ASSERT(state);
 
-  // set some CAD parameters - will be overwritten whel calling CAD anyway
+  // set some CAD parameters - will be overwritten when calling CAD anyway
   data[0] = RADIOLIB_SX126X_CAD_ON_8_SYMB;
   data[1] = this->spreadingFactor + 13;
   data[2] = RADIOLIB_SX126X_CAD_PARAM_DET_MIN;
diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp
index b903343c..30ffe5ee 100644
--- a/src/protocols/LoRaWAN/LoRaWAN.cpp
+++ b/src/protocols/LoRaWAN/LoRaWAN.cpp
@@ -37,6 +37,10 @@ LoRaWANNode::LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band) {
   this->startChannel = -1;
   this->numChannels = -1;
   this->backupFreq = this->band->backupChannel.freqStart;
+  this->difsSlots = 2;
+  this->backoffMax = 6;
+  this->enableCSMA = false;
+
 }
 
 void LoRaWANNode::wipe() {
@@ -44,6 +48,13 @@ void LoRaWANNode::wipe() {
   mod->hal->wipePersistentStorage();
 }
 
+void LoRaWANNode::setCSMA(uint8_t backoffMax, uint8_t difsSlots, bool enableCSMA) {
+    this->backoffMax = backoffMax;
+    this->difsSlots = difsSlots;
+    this->enableCSMA = enableCSMA;
+}
+
+
 int16_t LoRaWANNode::restoreOTAA() {
   int16_t state = this->setPhyProperties();
   RADIOLIB_ASSERT(state);
@@ -637,6 +648,11 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) {
     LoRaWANNode::hton<uint32_t>(&uplinkMsg[uplinkMsgLen - sizeof(uint32_t)], micF);
   }
 
+  // perform CSMA if enabled.
+  if (enableCSMA) {
+    performCSMA();
+  }
+
   RADIOLIB_DEBUG_PRINTLN("uplinkMsg:");
   RADIOLIB_DEBUG_HEXDUMP(uplinkMsg, uplinkMsgLen);
 
@@ -1762,4 +1778,52 @@ void LoRaWANNode::hton(uint8_t* buff, T val, size_t size) {
   }
 }
 
-#endif
+// The following function enables LMAC, a CSMA scheme for LoRa as specified 
+// in the LoRa Alliance Technical Recommendation #13.
+// A user may enable CSMA to provide frames an additional layer of protection from interference.
+// https://resources.lora-alliance.org/technical-recommendations/tr013-1-0-0-csma
+void LoRaWANNode::performCSMA() {
+    
+    // Compute initial random back-off. 
+    // When BO is reduced to zero, the function returns and the frame is transmitted.
+    uint32_t BO = this->phyLayer->random(1, this->backoffMax + 1);
+
+    while (BO > 0) {
+        // DIFS: Check channel for DIFS_slots
+        bool channelFreeDuringDIFS = true;
+        for (uint8_t i = 0; i < this->difsSlots; i++) {
+            if (performCAD()) {
+                RADIOLIB_DEBUG_PRINTLN("OCCUPIED CHANNEL DURING DIFS");
+                channelFreeDuringDIFS = false;
+                // Channel is occupied during DIFS, hop to another.
+                this->setupChannels();
+                break;
+            }
+        }
+
+        // Start reducing BO counter if DIFS slot was free.
+        if (channelFreeDuringDIFS) {
+            // Continue decrementing BO with per each CAD reporting free channel.
+            while (BO > 0) {
+                if (performCAD()) {
+                    RADIOLIB_DEBUG_PRINTLN("OCCUPIED CHANNEL DURING BO");
+                    // Channel is busy during CAD, hop to another and return to DIFS state again.
+                    this->setupChannels();
+                    break;  // Exit loop. Go back to DIFS state.
+                }
+                BO--;  // Decrement BO by one if channel is free
+            }
+        }
+    }
+}
+
+bool LoRaWANNode::performCAD() {
+    int16_t state = this->phyLayer->scanChannel();
+
+    if ((state == RADIOLIB_PREAMBLE_DETECTED) || (state == RADIOLIB_LORA_DETECTED)) {
+        return true; // Channel is busy
+    }
+    return false; // Channel is free
+}
+
+#endif
\ No newline at end of file
diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h
index 65efe7eb..82b08127 100644
--- a/src/protocols/LoRaWAN/LoRaWAN.h
+++ b/src/protocols/LoRaWAN/LoRaWAN.h
@@ -296,6 +296,17 @@ class LoRaWANNode {
         (e.g. 8 for US915 FSB2 used by TTN). By default -1 (no channel offset). */
     int8_t numChannels;
 
+    /*! \brief Num of Back Off(BO) slots to be decremented after DIFS phase. 0 to disable BO.
+        A random BO avoids collisions in the case where two or more nodes start the CSMA
+        process at the same time. */
+    uint8_t backoffMax;
+
+    /*! \brief Num of CADs to estimate a clear CH. */
+    uint8_t difsSlots;
+
+    /*! \brief enable/disable CSMA for LoRaWAN. */
+    bool enableCSMA;
+
     /*!
       \brief Default constructor.
       \param phy Pointer to the PhysicalLayer radio module.
@@ -309,6 +320,14 @@ class LoRaWANNode {
     */
     void wipe();
 
+    /*!
+      \brief Configures CSMA for LoRaWAN as per TR-13, LoRa Alliance.
+      \param backoffMax Num of BO slots to be decremented after DIFS phase. 0 to disable BO.
+      \param difsSlots Num of CADs to estimate a clear CH.
+      \param enableCSMA enable/disable CSMA for LoRaWAN.
+    */
+    void setCSMA(uint8_t backoffMax, uint8_t difsSlots, bool enableCSMA = false);
+
     /*!
       \brief Restore OTAA session by loading information from persistent storage.
       \returns \ref status_codes
@@ -502,6 +521,12 @@ class LoRaWANNode {
     // host-to-network conversion method - takes data from host variable and and converts it to network packet endians
     template<typename T>
     static void hton(uint8_t* buff, T val, size_t size = 0);
+
+    // perform a single CAD operation for the under SF/CH combination. Returns either busy or otherwise.
+    bool performCAD();
+
+    // Performs CSMA as per LoRa Alliance Technical Reccomendation 13 (TR-013).
+    void performCSMA();
 };
 
 #endif