Fork of Semtech LoRaWAN stack

Fork of LoRaWAN-lib by canuck lehead

Branch:
v4.2.0
Revision:
8:4816c8449bf2
Parent:
6:d7a34ded7c87
Child:
9:ad93de20d720
--- a/LoRaMac.cpp	Wed May 18 11:19:24 2016 +0000
+++ b/LoRaMac.cpp	Mon Aug 22 20:30:06 2016 -0400
@@ -317,7 +317,7 @@
 /*!
  * Up/Down link data rates offset definition
  */
-const int8_t datarateOffsets[16][4] =
+const uint8_t datarateOffsets[16][4] =
 {
     { DR_10, DR_9 , DR_8 , DR_8  }, // DR_0
     { DR_11, DR_10, DR_9 , DR_8  }, // DR_1
@@ -369,6 +369,39 @@
  * Contains the channels which remain to be applied.
  */
 static uint16_t ChannelsMaskRemaining[6];
+
+#if defined( USE_BAND_915 ) 
+/*!
+ *  Last join request sub-band 
+ */
+static int8_t LastJoinBlock;
+
+/*!
+ * Next join sub-band block
+ */
+static uint8_t  NextJoinBlock;
+
+/*!
+ * Mask of 125 KHz sub-bands not used for join transmit 
+ */
+static uint8_t  JoinBlocksRemaining;
+
+/*!
+ * Mask of 500 KHz sub-bands not used for join transmit 
+ */
+static uint8_t  Join500KHzRemaining;
+
+#ifndef JOIN_BLOCK_ORDER
+#define JOIN_BLOCK_ORDER { -1, -1, -1, -1, -1, -1, -1, -1 }
+#endif
+
+/*!
+ * join sub-band block order 
+ */
+static int8_t JoinBlock[8] = JOIN_BLOCK_ORDER;
+
+#endif
+
 #else
     #error "Please define a frequency band in the compiler options."
 #endif
@@ -560,6 +593,50 @@
  */
 LoRaMacFlags_t LoRaMacFlags;
 
+/* LoRaWAN 1.1 Section 9 Retransmission back-off 
+ *
+ * Applies to  uplink frames that:
+ * - Require acknowledgement or answer, and are 
+ *   retransmitted by the device if the answer is not received
+ * and
+ * - Can be triggered by an external event causing synchronization
+ *
+ * Transmission duty-cylce for such messages shall respect the local regulations
+ * and the following limits 
+ * - Aggregated during the first hour                36 seconds
+ * - Aggregated during the next 10 hours             36 seconds
+ *  - After the first 11 hours aggregated over 24h    8.7seconds
+ */
+#ifndef JOIN_RETRANSMISSION_DCYCLE1   
+#define JOIN_RETRANSMISSION_DCYCLE1   { 3600, 1000000 } // (period, onAirTimeMax)
+#endif
+
+#ifndef JOIN_RETRANSMISSION_DCYCLE2   
+#define JOIN_RETRANSMISSION_DCYCLE2   { 10*3600, 36000000 } // (period, onAirTimeMax)
+#endif
+
+#ifndef JOIN_RETRANSMISSION_DCYCLE3   
+#define JOIN_RETRANSMISSION_DCYCLE3   { 24*3600, 8700000 } // (period, onAirTimeMax)
+#endif
+
+LoRaMacRetransmissionDCycle_t JoinReTransmitDCycle[JOIN_NB_RETRANSMISSION_DCYCLES] = 
+{
+    JOIN_RETRANSMISSION_DCYCLE1, 
+    JOIN_RETRANSMISSION_DCYCLE2, 
+    JOIN_RETRANSMISSION_DCYCLE3
+};
+
+/*!
+ * Uptime of the last sent join request
+ */
+static TimerTime_t LastJoinTxTime;
+
+/*!
+ * Aggregated join request time on air 
+ */
+static TimerTime_t JoinAggTimeOnAir; 
+
+
 /*!
  * \brief Function to be executed on Radio Tx Done event
  */
@@ -748,13 +825,15 @@
  *
  * \param [IN] adrEnabled Specify whether ADR is on or off
  *
- * \param [IN] updateChannelMask Set to true, if the channel masks shall be updated
+ * \param [IN] isTx Set to true if called by transmit, set to false if a dry run
  *
  * \param [OUT] datarateOut Reports the datarate which will be used next
  *
+ * \param [OUT] txpowerOut Reports the tx power which will be used next
+ *
  * \retval Returns the state of ADR ack request
  */
-static bool AdrNextDr( bool adrEnabled, bool updateChannelMask, int8_t* datarateOut );
+static bool AdrNextDr( bool adrEnabled, bool isTx, int8_t* datarateOut, int8_t* txpowerOut );
 
 /*!
  * \brief Disables channel in a specified channel mask
@@ -842,6 +921,13 @@
     // Update Aggregated last tx done time
     AggregatedLastTxDoneTime = curTime;
 
+    // Update join tx done and time on air 
+    if( ( LoRaMacFlags.Bits.MlmeReq == 1 ) && ( MlmeConfirm.MlmeRequest == MLME_JOIN ) )
+    {
+        JoinAggTimeOnAir += TxTimeOnAir;
+        LastJoinTxTime    = curTime;
+    }
+
     if( IsRxWindowsEnabled == true )
     {
         TimerSetValue( &RxWindowTimer1, RxWindow1Delay );
@@ -1019,7 +1105,9 @@
 #endif
                 MlmeConfirm.Status = LORAMAC_EVENT_INFO_STATUS_OK;
                 IsLoRaMacNetworkJoined = true;
-                ChannelsDatarate = ChannelsDefaultDatarate;
+               
+                // Do not change the datarate
+                // ChannelsDatarate = ChannelsDefaultDatarate;
             }
             else
             {
@@ -1191,11 +1279,6 @@
                         }
                     }
 
-                    if( fCtrl.Bits.FOptsLen > 0 )
-                    {
-                        // Decode Options field MAC commands
-                        ProcessMacCommands( payload, 8, appPayloadStartIndex, snr );
-                    }
                     if( ( ( size - 4 ) - appPayloadStartIndex ) > 0 )
                     {
                         port = payload[appPayloadStartIndex++];
@@ -1205,19 +1288,32 @@
 
                         if( port == 0 )
                         {
-                            LoRaMacPayloadDecrypt( payload + appPayloadStartIndex,
-                                                   frameLen,
-                                                   nwkSKey,
-                                                   address,
-                                                   DOWN_LINK,
-                                                   downLinkCounter,
-                                                   LoRaMacRxPayload );
-
-                            // Decode frame payload MAC commands
-                            ProcessMacCommands( LoRaMacRxPayload, 0, frameLen, snr );
+                            if( fCtrl.Bits.FOptsLen == 0 )
+                            {
+                                LoRaMacPayloadDecrypt( payload + appPayloadStartIndex,
+                                                       frameLen,
+                                                       nwkSKey,
+                                                       address,
+                                                       DOWN_LINK,
+                                                       downLinkCounter,
+                                                       LoRaMacRxPayload );
+
+                                // Decode frame payload MAC commands
+                                ProcessMacCommands( LoRaMacRxPayload, 0, frameLen, snr );
+                            }
+                            else
+                            {
+                                skipIndication = true;
+                            }
                         }
                         else
                         {
+                            if( fCtrl.Bits.FOptsLen > 0 )
+                            {
+                                // Decode Options field MAC commands. Omit the fPort.
+                                ProcessMacCommands( payload, 8, appPayloadStartIndex - 1, snr );
+                            }
+
                             LoRaMacPayloadDecrypt( payload + appPayloadStartIndex,
                                                    frameLen,
                                                    appSKey,
@@ -1234,6 +1330,15 @@
                             }
                         }
                     }
+                    else
+                    {
+                        if( fCtrl.Bits.FOptsLen > 0 )
+                        {
+                            // Decode Options field MAC commands
+                            ProcessMacCommands( payload, 8, appPayloadStartIndex, snr );
+                        }
+                    }
+
                     if( skipIndication == false )
                     {
                         LoRaMacFlags.Bits.McpsInd = 1;
@@ -1676,6 +1781,91 @@
     }
 }
 
+#if defined( USE_BAND_915 ) 
+uint8_t GetNextJoinChannel( uint8_t* enabledChannels, uint8_t nbEnabledChannels )
+{ 
+    int8_t   channel = -1;
+    uint8_t  block;
+    uint8_t  i;
+
+    // Use 125KHz channel
+    if( ChannelsDatarate < DR_4 )
+    {
+        block = JoinBlock[NextJoinBlock]; 
+        NextJoinBlock = (NextJoinBlock + 1) % 8;
+
+        // If next block is greater than max block then randomly select the next block   
+        if( block >= 8 )
+            block = randr(0, 7);
+
+        // Start search for next join channel at the selected block
+        for(i = 0; ( i < 8 ) && ( channel == -1 ); i++)
+        {
+            uint8_t curBlock = (block + i) % 8;
+            uint8_t chMask;
+
+            // Cycle through all blocks before using a previously used block 
+            if( ( JoinBlocksRemaining == 0 ) || ( ( JoinBlocksRemaining & ( 1 <<  curBlock ) ) != 0) ) 
+            {
+                chMask = ( ChannelsMaskRemaining[curBlock/2] >> ( curBlock & 1 ? 8 : 0 ) ) & 0xff; 
+                if( chMask != 0)
+                { 
+                    channel = randr(0, 7); 
+                    for(uint8_t i = 0; ( i < 8 ); i++)
+                    {
+                        if( ( chMask & ( 1 << channel ) ) != 0 )
+                        {
+                            channel = channel + ( curBlock * 8 );
+                            JoinBlocksRemaining &= ~(1 << curBlock);
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+        
+        // No next channel case should never happen since ScheduleTx has already 
+        // checked for at least one enabled channel and if none re-enabled all channels. 
+        // But if we find ourselves without a channel then randomly select one
+        if ( i >= 8 )
+        {
+            channel = randr( 0, 63 );
+        }
+    }
+    // Use 500 KHz channel
+    else 
+    {
+        channel = randr( 0, 7 );
+        for(i = 0; i < 8; i++)
+        {
+            uint8_t curChannel = (channel + i) % 8;
+
+            if( ( Join500KHzRemaining == 0 ) || ( ( Join500KHzRemaining & ( 1 << curChannel ) ) != 0 ) )
+            {
+                channel = 64 + curChannel;
+                Join500KHzRemaining &= ~(1 << channel);
+                break;
+            }
+        }
+    }
+
+    for( i = 0; i < nbEnabledChannels; i++ )
+    {
+        if( enabledChannels[i] == channel )
+            break;
+    }
+
+    if( i == nbEnabledChannels )
+    {
+        channel = enabledChannels[randr( 0, nbEnabledChannels - 1 )];
+    }
+
+    LastJoinBlock = channel / 8;
+
+    return channel;
+}
+#endif
+
 static bool SetNextChannel( TimerTime_t* time )
 {
     uint8_t nbEnabledChannels = 0;
@@ -1777,7 +1967,39 @@
 
     if( nbEnabledChannels > 0 )
     {
-        Channel = enabledChannels[randr( 0, nbEnabledChannels - 1 )];
+#if defined( USE_BAND_915 ) 
+        if ( IsLoRaMacNetworkJoined == false )
+        {
+            Channel = GetNextJoinChannel(enabledChannels, nbEnabledChannels);
+        }
+        // Send first uplink on channel from same sub-band as the join 
+        else if( ( UpLinkCounter == 1 ) && ( LastJoinBlock != -1 ) )
+        {
+            uint8_t i;
+            uint8_t blockFirstChannel = LastJoinBlock*8;
+
+            Channel = randr( blockFirstChannel, blockFirstChannel + 7 );
+
+            // Check channel is enabled
+            for( i = 0; i < nbEnabledChannels; i++ )
+            {
+                if( Channel == enabledChannels[i] )
+                    break;
+            }
+
+            // If channel is not enabled fallback to selecting from the list of 
+            // enabled  channels
+            if (i == nbEnabledChannels)
+            {
+                Channel = enabledChannels[randr( 0, nbEnabledChannels - 1 )];
+            }
+        }
+        else
+#endif
+        { 
+            Channel = enabledChannels[randr( 0, nbEnabledChannels - 1 )];
+        }
+
 #if defined( USE_BAND_915 ) || defined( USE_BAND_915_HYBRID )
         if( Channel < ( LORA_MAX_NB_CHANNELS - 8 ) )
         {
@@ -2028,82 +2250,75 @@
     return true;
 }
 
-static bool AdrNextDr( bool adrEnabled, bool updateChannelMask, int8_t* datarateOut )
+static bool AdrNextDr( bool adrEnabled, bool isTx, int8_t* datarateOut, int8_t* txpowerOut )
 {
     bool adrAckReq = false;
     int8_t datarate = ChannelsDatarate;
+    int8_t txpower  = ChannelsTxPower;
 
     if( adrEnabled == true )
     {
-        if( datarate == LORAMAC_TX_MIN_DATARATE )
-        {
-            AdrAckCounter = 0;
-            adrAckReq = false;
-        }
-        else
+        /* Request ADR Ack Request (including at lowest available datarate) */
+        adrAckReq = AdrAckCounter >= ADR_ACK_LIMIT ? true : false;
+
+        if( AdrAckCounter > (ADR_ACK_LIMIT + ADR_ACK_DELAY) )
         {
-            if( AdrAckCounter >= ADR_ACK_LIMIT )
-            {
-                adrAckReq = true;
-            }
-            else
-            {
-                adrAckReq = false;
-            }
-            if( AdrAckCounter >= ( ADR_ACK_LIMIT + ADR_ACK_DELAY ) )
-            {
-                if( ( ( AdrAckCounter - ADR_ACK_DELAY ) % ADR_ACK_LIMIT ) == 0 )
-                {
+            // Step up power
+            txpower = LORAMAC_DEFAULT_TX_POWER;
+
+            // isTx=true when preparing to transmit the next frame
+            if(isTx == true)
+                AdrAckCounter = 0;
+
 #if defined( USE_BAND_433 ) || defined( USE_BAND_780 ) || defined( USE_BAND_868 )
-                    if( datarate > LORAMAC_TX_MIN_DATARATE )
-                    {
-                        datarate--;
-                    }
-                    if( datarate == LORAMAC_TX_MIN_DATARATE )
-                    {
-                        if( updateChannelMask == true )
-                        {
-
-                            // Re-enable default channels LC1, LC2, LC3
-                            ChannelsMask[0] = ChannelsMask[0] | ( LC( 1 ) + LC( 2 ) + LC( 3 ) );
-                        }
-                    }
+            if( datarate > LORAMAC_TX_MIN_DATARATE )
+            {
+                datarate--;
+            }
+            if( datarate == LORAMAC_TX_MIN_DATARATE )
+            {
+                if( isTx == true )
+                {
+                    // Re-enable default channels LC1, LC2, LC3
+                    ChannelsMask[0] = ChannelsMask[0] | ( LC( 1 ) + LC( 2 ) + LC( 3 ) );
+                }
+            }
 #elif defined( USE_BAND_915 ) || defined( USE_BAND_915_HYBRID )
-                    if( ( datarate > LORAMAC_TX_MIN_DATARATE ) && ( datarate == DR_8 ) )
-                    {
-                        datarate = DR_4;
-                    }
-                    else if( datarate > LORAMAC_TX_MIN_DATARATE )
-                    {
-                        datarate--;
-                    }
-                    if( datarate == LORAMAC_TX_MIN_DATARATE )
-                    {
-                        if( updateChannelMask == true )
-                        {
+            if( datarate > LORAMAC_TX_MIN_DATARATE ) 
+            {
+                datarate--;
+            }
+            else if( isTx == true )
+            {
 #if defined( USE_BAND_915 )
-                            // Re-enable default channels
-                            ChannelsMask[0] = 0xFFFF;
-                            ChannelsMask[1] = 0xFFFF;
-                            ChannelsMask[2] = 0xFFFF;
-                            ChannelsMask[3] = 0xFFFF;
-                            ChannelsMask[4] = 0x00FF;
-                            ChannelsMask[5] = 0x0000;
+                // Re-enable default channels
+                ChannelsMask[0] = 0xFFFF;
+                ChannelsMask[1] = 0xFFFF;
+                ChannelsMask[2] = 0xFFFF;
+                ChannelsMask[3] = 0xFFFF;
+                ChannelsMask[4] = 0x00FF;
+                ChannelsMask[5] = 0x0000;
 #else // defined( USE_BAND_915_HYBRID )
-                            // Re-enable default channels
-                            ReenableChannels( ChannelsMask[4], ChannelsMask );
+                // Re-enable default channels
+                ChannelsMask[0] = 0x00FF;
+                ChannelsMask[1] = 0x0000;
+                ChannelsMask[2] = 0x0000;
+                ChannelsMask[3] = 0x0000;
+                ChannelsMask[4] = 0x0001;
+                ChannelsMask[5] = 0x0000;
 #endif
-                        }
-                    }
+            }
 #else
 #error "Please define a frequency band in the compiler options."
 #endif
-                }
-            }
         }
     }
 
-    *datarateOut = datarate;
+    if(datarateOut != NULL)
+        *datarateOut = datarate;
+
+    if(txpowerOut != NULL)
+        *txpowerOut = txpower;
 
     return adrAckReq;
 }
@@ -2286,6 +2501,10 @@
                                 }
                             }
                         }
+
+                        // channel mask applied to 500 kHz channels
+                        channelsMask[4] = chMask;
+                        chMaskCntl = 4;
                     }
                     else if( chMaskCntl == 7 )
                     {
@@ -2294,13 +2513,18 @@
                         channelsMask[1] = 0x0000;
                         channelsMask[2] = 0x0000;
                         channelsMask[3] = 0x0000;
+
+                        // channel mask applied to 500 kHz channels
+                        channelsMask[4] = chMask;
+                        chMaskCntl = 4;
                     }
                     else if( chMaskCntl == 5 )
                     {
                         // RFU
                         status &= 0xFE; // Channel mask KO
                     }
-                    else
+
+                    if(status == 0xFF)
                     {
                         for( uint8_t i = 0; i < 16; i++ )
                         {
@@ -2660,7 +2884,7 @@
                 return LORAMAC_STATUS_NO_NETWORK_JOINED; // No network has been joined yet
             }
 
-            fCtrl->Bits.AdrAckReq = AdrNextDr( fCtrl->Bits.Adr, true, &ChannelsDatarate );
+            fCtrl->Bits.AdrAckReq = AdrNextDr( fCtrl->Bits.Adr, true, &ChannelsDatarate, &ChannelsTxPower );
 
             if( ValidatePayloadLength( fBufferSize, ChannelsDatarate, MacCommandsBufferIndex ) == false )
             {
@@ -2850,6 +3074,31 @@
     IsLoRaMacNetworkJoined = false;
     LoRaMacState = MAC_IDLE;
 
+    MulticastChannels = NULL;
+    IsUpLinkCounterFixed = false;
+    IsRxWindowsEnabled = true;
+    IsLoRaMacNetworkJoined = false;
+    AdrCtrlOn = false;
+    AdrAckCounter = 0;
+    NodeAckRequested = false;
+    SrvAckRequested = false;
+    MacCommandsInNextTx = false;
+    MacCommandsBufferIndex = 0;
+    AckTimeoutRetries = 1;
+    AckTimeoutRetriesCounter = 1;
+    AckTimeoutRetry = false;
+    TxTimeOnAir = 0;
+    RxSlot = 0;
+    //Rx2Channel = RX_WND_2_CHANNEL;
+    Rx1DrOffset = 0;
+    ChannelsTxPower = LORAMAC_DEFAULT_TX_POWER;
+    ChannelsDatarate = LORAMAC_DEFAULT_DATARATE;
+    ChannelsDefaultDatarate = LORAMAC_DEFAULT_DATARATE;
+    ChannelsNbRep = 1;
+    ChannelsNbRepCounter = 0;
+    MaxDCycle = 0;
+    
+
 #if defined( USE_BAND_433 )
     ChannelsMask[0] = LC( 1 ) + LC( 2 ) + LC( 3 );
 #elif defined( USE_BAND_780 )
@@ -2923,6 +3172,16 @@
     JoinAcceptDelay1 = JOIN_ACCEPT_DELAY1;
     JoinAcceptDelay2 = JOIN_ACCEPT_DELAY2;
 
+#if defined( USE_BAND_915 ) 
+    NextJoinBlock = 0;
+    LastJoinBlock = -1;
+    JoinBlocksRemaining = 0xff;
+    Join500KHzRemaining = 0xff;
+#endif
+
+    LastJoinTxTime  = 0;
+    JoinAggTimeOnAir = 0; 
+
     TimerInit( &MacStateCheckTimer, OnMacStateCheckTimerEvent );
     TimerSetValue( &MacStateCheckTimer, MAC_STATE_CHECK_TIMEOUT );
 
@@ -2961,7 +3220,7 @@
         return LORAMAC_STATUS_PARAMETER_INVALID;
     }
 
-    AdrNextDr( AdrCtrlOn, false, &datarate );
+    AdrNextDr( AdrCtrlOn, false, &datarate, NULL );
 
     if( RepeaterSupport == true )
     {
@@ -3176,6 +3435,14 @@
         case MIB_NETWORK_JOINED:
         {
             IsLoRaMacNetworkJoined = mibSet->Param.IsNetworkJoined;
+#if defined( USE_BAND_915 ) || defined( USE_BAND_915_HYBRID )
+            if( IsLoRaMacNetworkJoined == false )
+            {
+                NextJoinBlock = 0;
+                JoinBlocksRemaining = 0xff;
+                Join500KHzRemaining = 0xff;
+            }
+#endif
             break;
         }
         case MIB_ADR:
@@ -3628,10 +3895,23 @@
             LoRaMacAppEui = mlmeRequest->Req.Join.AppEui;
             LoRaMacAppKey = mlmeRequest->Req.Join.AppKey;
 
+            if( LoRaMacCalcJoinBackOff( ) != 0 )
+            {
+                return LORAMAC_STATUS_TX_DCYCLE_EXCEEDED;
+            }
+
             macHdr.Value = 0;
             macHdr.Bits.MType  = FRAME_TYPE_JOIN_REQ;
 
-            IsLoRaMacNetworkJoined = false;
+#if defined( USE_BAND_915 ) || defined( USE_BAND_915_HYBRID )
+            if ( IsLoRaMacNetworkJoined == true )
+            {
+                NextJoinBlock = 0;
+                JoinBlocksRemaining = 0xff;
+                Join500KHzRemaining = 0xff;
+                IsLoRaMacNetworkJoined = false;
+            }
+#endif
 
 #if defined( USE_BAND_915 ) || defined( USE_BAND_915_HYBRID )
 #if defined( USE_BAND_915 )
@@ -3774,6 +4054,67 @@
     return status;
 }
 
+TimerTime_t LoRaMacCalcJoinBackOff( )
+{
+    TimerTime_t timeOff = 0; 
+    TimerTime_t uptime; 
+    TimerTime_t currDCycleEndTime;
+    TimerTime_t prevDCycleEndTime; 
+    TimerTime_t period;
+    TimerTime_t onAirTimeMax;
+    uint8_t     dcycleNum;
+
+    // Check retransmit duty-cycle exists
+    if ( JOIN_NB_RETRANSMISSION_DCYCLES == 0 )
+        return 0;
+
+    uptime = TimerGetCurrentTime( ) / 1e6;
+
+    // Get uptime dutycycle 
+    prevDCycleEndTime = 0;
+    currDCycleEndTime = 0;
+    for(dcycleNum = 0; dcycleNum < JOIN_NB_RETRANSMISSION_DCYCLES; dcycleNum++)
+    {                
+        prevDCycleEndTime  = currDCycleEndTime;
+        currDCycleEndTime += JoinReTransmitDCycle[dcycleNum].period;  
+
+        bool isCurrentPeriod = uptime < currDCycleEndTime;
+
+        if( isCurrentPeriod || ( dcycleNum == ( JOIN_NB_RETRANSMISSION_DCYCLES-1 ) ) )
+        {
+            period = JoinReTransmitDCycle[dcycleNum].period;
+            onAirTimeMax = JoinReTransmitDCycle[dcycleNum].onAirTimeMax;
+            if( isCurrentPeriod == true ) 
+                break;
+        }
+    }
+
+    // Clear aggregate on air time if last join occured during a previous period 
+    if( LastJoinTxTime < prevDCycleEndTime ) 
+    {
+        JoinAggTimeOnAir = 0;
+    }
+    // For last dutycyle, the current & previous period end time must be calculated 
+    else if( dcycleNum ==  JOIN_NB_RETRANSMISSION_DCYCLES )  
+    {
+        if( ( TimerGetElapsedTime( LastJoinTxTime )/1e6 ) >= period )
+        {
+            JoinAggTimeOnAir = 0;
+        }
+    }
+
+    if ( JoinAggTimeOnAir >= onAirTimeMax ) 
+    {
+        // current period elapsed time 
+        TimerTime_t elapsedTime =  ( uptime - prevDCycleEndTime ) % period; 
+
+        // time off is remaining time until beginning of next period 
+        timeOff = ( period - elapsedTime ) * 1e6; 
+    }
+
+    return timeOff;
+}
+
 void LoRaMacTestRxWindowsOn( bool enable )
 {
     IsRxWindowsEnabled = enable;