text: /*!
   * \file      RegionCommon.c
   *
   * \brief     LoRa MAC common region implementation
   *
   * \copyright Revised BSD License, see section \ref LICENSE.
   *
   * \code
   *                ______                              _
   *               / _____)             _              | |
   *              ( (____  _____ ____ _| |_ _____  ____| |__
   *               \____ \| ___ |    (_   _) ___ |/ ___)  _ \
   *               _____) ) ____| | | || |_| ____( (___| | | |
   *              (______/|_____)_|_|_| \__)_____)\____)_| |_|
   *              (C)2013-2017 Semtech
   *
   *               ___ _____ _   ___ _  _____ ___  ___  ___ ___
   *              / __|_   _/_\ / __| |/ / __/ _ \| _ \/ __| __|
   *              \__ \ | |/ _ \ (__| ' <| _| (_) |   / (__| _|
   *              |___/ |_/_/ \_\___|_|\_\_| \___/|_|_\\___|___|
   *              embedded.connectivity.solutions===============
   *
   * \endcode
   *
   * \author    Miguel Luis ( Semtech )
   *
   * \author    Gregory Cristian ( Semtech )
   *
   * \author    Daniel Jaeckle ( STACKFORCE )
   */
  /**
    ******************************************************************************
    *
    *          Portions COPYRIGHT 2020 STMicroelectronics
    *
    * @file    RegionCommon.c
    * @author  MCD Application Team
    * @brief   LoRa MAC common region implementation
    ******************************************************************************
    */
  #include <math.h>
  #include "radio.h"
  #include "utilities.h"
  #include "RegionCommon.h"
  #include "systime.h"
  #include "mw_log_conf.h"
  
  #define BACKOFF_DC_1_HOUR                   100
  #define BACKOFF_DC_10_HOURS                 1000
  #define BACKOFF_DC_24_HOURS                 10000
  
  #define BACKOFF_DUTY_CYCLE_1_HOUR_IN_S      3600
  #define BACKOFF_DUTY_CYCLE_10_HOURS_IN_S    ( BACKOFF_DUTY_CYCLE_1_HOUR_IN_S + ( BACKOFF_DUTY_CYCLE_1_HOUR_IN_S * 10 ) )
  #define BACKOFF_DUTY_CYCLE_24_HOURS_IN_S    ( BACKOFF_DUTY_CYCLE_10_HOURS_IN_S + ( BACKOFF_DUTY_CYCLE_1_HOUR_IN_S * 24 ) )
  #define BACKOFF_24_HOURS_IN_S               ( BACKOFF_DUTY_CYCLE_1_HOUR_IN_S * 24 )
  
  #ifndef DUTY_CYCLE_TIME_PERIOD
  /*!
   * Default duty cycle observation time period
   *
   * \remark The ETSI observation time period is 1 hour (3600000 ms) but, the implemented algorithm may violate the
   *         defined duty-cycle restrictions. In order to ensure that these restrictions never get violated we changed the
   *         default duty cycle observation time period to 1/2 hour (1800000 ms).
   */
  #define DUTY_CYCLE_TIME_PERIOD              1800000
  #endif
  
  /*!
   * \brief Returns `N / D` rounded to the smallest integer value greater than or equal to `N / D`
   *
   * \warning when `D == 0`, the result is undefined
   *
   * \remark `N` and `D` can be signed or unsigned
   *
   * \param [IN] N the numerator, which can have any sign
   * \param [IN] D the denominator, which can have any sign
   * \retval N / D with any fractional part rounded to the smallest integer value greater than or equal to `N / D`
   */
  #define DIV_CEIL( N, D )                                                       \
      (                                                                          \
          ( N > 0 ) ?                                                            \
          ( ( ( N ) + ( D ) - 1 ) / ( D ) ) :                                    \
          ( ( N ) / ( D ) )                                                      \
      )
  
  static uint16_t GetDutyCycle( Band_t* band, bool joined, SysTime_t elapsedTimeSinceStartup )
  {
      uint16_t dutyCycle = band->DCycle;
  
      if( joined == false )
      {
          uint16_t joinDutyCycle = BACKOFF_DC_24_HOURS;
  
          if( elapsedTimeSinceStartup.Seconds < BACKOFF_DUTY_CYCLE_1_HOUR_IN_S )
          {
              joinDutyCycle = BACKOFF_DC_1_HOUR;
          }
          else if( elapsedTimeSinceStartup.Seconds < BACKOFF_DUTY_CYCLE_10_HOURS_IN_S )
          {
              joinDutyCycle = BACKOFF_DC_10_HOURS;
          }
          else
          {
              joinDutyCycle = BACKOFF_DC_24_HOURS;
          }
          // Take the most restrictive duty cycle
          dutyCycle = MAX( dutyCycle, joinDutyCycle );
      }
  
      // Prevent value of 0
      if( dutyCycle == 0 )
      {
          dutyCycle = 1;
      }
  
      return dutyCycle;
  }
  
  static uint16_t SetMaxTimeCredits( Band_t* band, bool joined, SysTime_t elapsedTimeSinceStartup,
                                     bool dutyCycleEnabled, bool lastTxIsJoinRequest )
  {
      uint16_t dutyCycle = band->DCycle;
      TimerTime_t maxCredits = DUTY_CYCLE_TIME_PERIOD;
      TimerTime_t elapsedTime = SysTimeToMs( elapsedTimeSinceStartup );
      SysTime_t timeDiff = { 0 };
  
      // Get the band duty cycle. If not joined, the function either returns the join duty cycle
      // or the band duty cycle, whichever is more restrictive.
      dutyCycle = GetDutyCycle( band, joined, elapsedTimeSinceStartup );
  
      if( joined == false )
      {
          if( dutyCycle == BACKOFF_DC_1_HOUR )
          {
              maxCredits = DUTY_CYCLE_TIME_PERIOD;
              band->LastMaxCreditAssignTime = elapsedTime;
          }
          else if( dutyCycle == BACKOFF_DC_10_HOURS )
          {
              maxCredits = DUTY_CYCLE_TIME_PERIOD * 10;
              band->LastMaxCreditAssignTime = elapsedTime;
          }
          else
          {
              maxCredits = DUTY_CYCLE_TIME_PERIOD * 24;
          }
  
          timeDiff = SysTimeSub( elapsedTimeSinceStartup, SysTimeFromMs( band->LastMaxCreditAssignTime ) );
  
          // Verify if we have to assign the maximum credits in cases
          // of the preconditions have changed.
          if( ( ( dutyCycleEnabled == false ) && ( lastTxIsJoinRequest == false ) ) ||
              ( band->MaxTimeCredits != maxCredits ) ||
              ( timeDiff.Seconds >= BACKOFF_24_HOURS_IN_S ) )
          {
              band->TimeCredits = maxCredits;
  
              if( elapsedTimeSinceStartup.Seconds >= BACKOFF_DUTY_CYCLE_24_HOURS_IN_S )
              {
                  timeDiff.Seconds = ( elapsedTimeSinceStartup.Seconds - BACKOFF_DUTY_CYCLE_24_HOURS_IN_S ) / BACKOFF_24_HOURS_IN_S;
                  timeDiff.Seconds *= BACKOFF_24_HOURS_IN_S;
                  timeDiff.Seconds += BACKOFF_DUTY_CYCLE_24_HOURS_IN_S;
                  timeDiff.SubSeconds = 0;
                  band->LastMaxCreditAssignTime = SysTimeToMs( timeDiff );
              }
          }
      }
      else
      {
          if( dutyCycleEnabled == false )
          {
              // Assign max credits when the duty cycle is disabled.
              band->TimeCredits = maxCredits;
          }
      }
  
      // Assign the max credits if its the first time
      if( band->LastBandUpdateTime == 0 )
      {
          band->TimeCredits = maxCredits;
      }
  
      // Setup the maximum allowed credits. We can assign them
      // safely all the time.
      band->MaxTimeCredits = maxCredits;
  
      return dutyCycle;
  }
  
  static uint16_t UpdateTimeCredits( Band_t* band, bool joined, bool dutyCycleEnabled,
                                     bool lastTxIsJoinRequest, SysTime_t elapsedTimeSinceStartup,
                                     TimerTime_t currentTime )
  {
      uint16_t dutyCycle = SetMaxTimeCredits( band, joined, elapsedTimeSinceStartup,
                                              dutyCycleEnabled, lastTxIsJoinRequest );
  
      if( joined == true )
      {
          // Apply a sliding window for the duty cycle with collection and speding
          // credits.
          band->TimeCredits += TimerGetElapsedTime( band->LastBandUpdateTime );
      }
  
      // Limit band credits to maximum
      if( band->TimeCredits > band->MaxTimeCredits )
      {
          band->TimeCredits = band->MaxTimeCredits;
      }
  
      // Synchronize update time
      band->LastBandUpdateTime = currentTime;
  
      return dutyCycle;
  }
  
  static uint8_t CountChannels( uint16_t mask, uint8_t nbBits )
  {
      uint8_t nbActiveBits = 0;
  
      for( uint8_t j = 0; j < nbBits; j++ )
      {
          if( ( mask & ( 1 << j ) ) == ( 1 << j ) )
          {
              nbActiveBits++;
          }
      }
      return nbActiveBits;
  }
  
  bool RegionCommonChanVerifyDr( uint8_t nbChannels, uint16_t* channelsMask, int8_t dr, int8_t minDr, int8_t maxDr, ChannelParams_t* channels )
  {
      if( RegionCommonValueInRange( dr, minDr, maxDr ) == 0 )
      {
          return false;
      }
  
      for( uint8_t i = 0, k = 0; i < nbChannels; i += 16, k++ )
      {
          for( uint8_t j = 0; j < 16; j++ )
          {
              if( ( ( channelsMask[k] & ( 1 << j ) ) != 0 ) )
              {// Check datarate validity for enabled channels
                  if( RegionCommonValueInRange( dr, ( channels[i + j].DrRange.Fields.Min & 0x0F ),
                                                    ( channels[i + j].DrRange.Fields.Max & 0x0F ) ) == 1 )
                  {
                      // At least 1 channel has been found we can return OK.
                      return true;
                  }
              }
          }
      }
      return false;
  }
  
  uint8_t RegionCommonValueInRange( int8_t value, int8_t min, int8_t max )
  {
      if( ( value >= min ) && ( value <= max ) )
      {
          return 1;
      }
      return 0;
  }
  
  bool RegionCommonChanDisable( uint16_t* channelsMask, uint8_t id, uint8_t maxChannels )
  {
      uint8_t index = id / 16;
  
      if( ( index > ( maxChannels / 16 ) ) || ( id >= maxChannels ) )
      {
          return false;
      }
  
      // Deactivate channel
      channelsMask[index] &= ~( 1 << ( id % 16 ) );
  
      return true;
  }
  
  uint8_t RegionCommonCountChannels( uint16_t* channelsMask, uint8_t startIdx, uint8_t stopIdx )
  {
      uint8_t nbChannels = 0;
  
      if( channelsMask == NULL )
      {
          return 0;
      }
  
      for( uint8_t i = startIdx; i < stopIdx; i++ )
      {
          nbChannels += CountChannels( channelsMask[i], 16 );
      }
  
      return nbChannels;
  }
  
  void RegionCommonChanMaskCopy( uint16_t* channelsMaskDest, uint16_t* channelsMaskSrc, uint8_t len )
  {
      if( ( channelsMaskDest != NULL ) && ( channelsMaskSrc != NULL ) )
      {
          for( uint8_t i = 0; i < len; i++ )
          {
              channelsMaskDest[i] = channelsMaskSrc[i];
          }
      }
  }
  
  void RegionCommonSetBandTxDone( Band_t* band, TimerTime_t lastTxAirTime, bool joined, SysTime_t elapsedTimeSinceStartup )
  {
      // Get the band duty cycle. If not joined, the function either returns the join duty cycle
      // or the band duty cycle, whichever is more restrictive.
      uint16_t dutyCycle = GetDutyCycle( band, joined, elapsedTimeSinceStartup );
  
      // Reduce with transmission time
      if( band->TimeCredits > ( lastTxAirTime * dutyCycle ) )
      {
          // Reduce time credits by the time of air
          band->TimeCredits -= ( lastTxAirTime * dutyCycle );
      }
      else
      {
          band->TimeCredits = 0;
      }
  }
  
  TimerTime_t RegionCommonUpdateBandTimeOff( bool joined, Band_t* bands,
                                             uint8_t nbBands, bool dutyCycleEnabled,
                                             bool lastTxIsJoinRequest, SysTime_t elapsedTimeSinceStartup,
                                             TimerTime_t expectedTimeOnAir )
  {
      TimerTime_t minTimeToWait = TIMERTIME_T_MAX;
      TimerTime_t currentTime = TimerGetCurrentTime( );
      TimerTime_t creditCosts = 0;
      uint16_t dutyCycle = 1;
      uint8_t validBands = 0;
  
      for( uint8_t i = 0; i < nbBands; i++ )
      {
          // Synchronization of bands and credits
          dutyCycle = UpdateTimeCredits( &bands[i], joined, dutyCycleEnabled,
                                         lastTxIsJoinRequest, elapsedTimeSinceStartup,
                                         currentTime );
  
          // Calculate the credit costs for the next transmission
          // with the duty cycle and the expected time on air
          creditCosts = expectedTimeOnAir * dutyCycle;
  
          // Check if the band is ready for transmission. Its ready,
          // when the duty cycle is off, or the TimeCredits of the band
          // is higher than the credit costs for the transmission.
          if( ( bands[i].TimeCredits > creditCosts ) ||
              ( ( dutyCycleEnabled == false ) && ( joined == true ) ) )
          {
              bands[i].ReadyForTransmission = true;
              // This band is a potential candidate for an
              // upcoming transmission, so increase the counter.
              validBands++;
          }
          else
          {
              // In this case, the band has not enough credits
              // for the next transmission.
              bands[i].ReadyForTransmission = false;
  
              if( bands[i].MaxTimeCredits > creditCosts )
              {
                  // The band can only be taken into account, if the maximum credits
                  // of the band are higher than the credit costs.
                  // We calculate the minTimeToWait among the bands which are not
                  // ready for transmission and which are potentially available
                  // for a transmission in the future.
                  minTimeToWait = MIN( minTimeToWait, ( creditCosts - bands[i].TimeCredits ) );
                  // This band is a potential candidate for an
                  // upcoming transmission (even if its time credits are not enough
                  // at the moment), so increase the counter.
                  validBands++;
              }
  
              // Apply a special calculation if the device is not joined.
              if( joined == false )
              {
                  SysTime_t backoffTimeRange = {
                      .Seconds    = 0,
                      .SubSeconds = 0,
                  };
                  // Get the backoff time range based on the duty cycle definition
                  if( dutyCycle == BACKOFF_DC_1_HOUR )
                  {
                      backoffTimeRange.Seconds = BACKOFF_DUTY_CYCLE_1_HOUR_IN_S;
                  }
                  else if( dutyCycle == BACKOFF_DC_10_HOURS )
                  {
                      backoffTimeRange.Seconds = BACKOFF_DUTY_CYCLE_10_HOURS_IN_S;
                  }
                  else
                  {
                      backoffTimeRange.Seconds = BACKOFF_DUTY_CYCLE_24_HOURS_IN_S;
                  }
                  // Calculate the time to wait.
                  if( elapsedTimeSinceStartup.Seconds > BACKOFF_DUTY_CYCLE_24_HOURS_IN_S )
                  {
                      backoffTimeRange.Seconds += BACKOFF_24_HOURS_IN_S * ( ( ( elapsedTimeSinceStartup.Seconds - BACKOFF_DUTY_CYCLE_24_HOURS_IN_S ) / BACKOFF_24_HOURS_IN_S ) + 1 );
                  }
                  // Calculate the time difference between now and the next range
                  backoffTimeRange  = SysTimeSub( backoffTimeRange, elapsedTimeSinceStartup );
                  minTimeToWait = SysTimeToMs( backoffTimeRange );
              }
          }
      }
  
  
      if( validBands == 0 )
      {
          // There is no valid band available to handle a transmission
          // in the given DUTY_CYCLE_TIME_PERIOD.
          return TIMERTIME_T_MAX;
      }
      return minTimeToWait;
  }
  
  uint8_t RegionCommonParseLinkAdrReq( uint8_t* payload, RegionCommonLinkAdrParams_t* linkAdrParams )
  {
      uint8_t retIndex = 0;
  
      if( payload[0] == SRV_MAC_LINK_ADR_REQ )
      {
          // Parse datarate and tx power
          linkAdrParams->Datarate = payload[1];
          linkAdrParams->TxPower = linkAdrParams->Datarate & 0x0F;
          linkAdrParams->Datarate = ( linkAdrParams->Datarate >> 4 ) & 0x0F;
          // Parse ChMask
          linkAdrParams->ChMask = ( uint16_t )payload[2];
          linkAdrParams->ChMask |= ( uint16_t )payload[3] << 8;
          // Parse ChMaskCtrl and nbRep
          linkAdrParams->NbRep = payload[4];
          linkAdrParams->ChMaskCtrl = ( linkAdrParams->NbRep >> 4 ) & 0x07;
          linkAdrParams->NbRep &= 0x0F;
  
          // LinkAdrReq has 4 bytes length + 1 byte CMD
          retIndex = 5;
      }
      return retIndex;
  }
  
  uint8_t RegionCommonLinkAdrReqVerifyParams( RegionCommonLinkAdrReqVerifyParams_t* verifyParams, int8_t* dr, int8_t* txPow, uint8_t* nbRep )
  {
      uint8_t status = verifyParams->Status;
      int8_t datarate = verifyParams->Datarate;
      int8_t txPower = verifyParams->TxPower;
      int8_t nbRepetitions = verifyParams->NbRep;
  
      // Handle the case when ADR is off.
      if( verifyParams->AdrEnabled == false )
      {
          // When ADR is off, we are allowed to change the channels mask
          nbRepetitions = verifyParams->CurrentNbRep;
          datarate =  verifyParams->CurrentDatarate;
          txPower =  verifyParams->CurrentTxPower;
      }
  
      if( status != 0 )
      {
          // Verify datarate. The variable phyParam. Value contains the minimum allowed datarate.
          if( RegionCommonChanVerifyDr( verifyParams->NbChannels, verifyParams->ChannelsMask, datarate,
                                        verifyParams->MinDatarate, verifyParams->MaxDatarate, verifyParams->Channels  ) == false )
          {
              status &= 0xFD; // Datarate KO
          }
  
          // Verify tx power
          if( RegionCommonValueInRange( txPower, verifyParams->MaxTxPower, verifyParams->MinTxPower ) == 0 )
          {
              // Verify if the maximum TX power is exceeded
              if( verifyParams->MaxTxPower > txPower )
              { // Apply maximum TX power. Accept TX power.
                  txPower = verifyParams->MaxTxPower;
              }
              else
              {
                  status &= 0xFB; // TxPower KO
              }
          }
      }
  
      // If the status is ok, verify the NbRep
      if( status == 0x07 )
      {
          if( nbRepetitions == 0 )
          { // Restore the default value according to the LoRaWAN specification
              nbRepetitions = 1;
          }
      }
  
      // Apply changes
      *dr = datarate;
      *txPow = txPower;
      *nbRep = nbRepetitions;
  
      return status;
  }
  
  uint32_t RegionCommonComputeSymbolTimeLoRa( uint8_t phyDr, uint32_t bandwidthInHz )
  {
      return ( 1 << phyDr ) * 1000000 / bandwidthInHz;
  }
  
  uint32_t RegionCommonComputeSymbolTimeFsk( uint8_t phyDrInKbps )
  {
      return 8000 / ( uint32_t )phyDrInKbps; // 1 symbol equals 1 byte
  }
  
  void RegionCommonComputeRxWindowParameters( uint32_t tSymbolInUs, uint8_t minRxSymbols, uint32_t rxErrorInMs, uint32_t wakeUpTimeInMs, uint32_t* windowTimeoutInSymbols, int32_t* windowOffsetInMs )
  {
      *windowTimeoutInSymbols = MAX( DIV_CEIL( ( ( 2 * minRxSymbols - 8 ) * tSymbolInUs + 2 * ( rxErrorInMs * 1000 ) ),  tSymbolInUs ), minRxSymbols ); // Computed number of symbols
      *windowOffsetInMs = ( int32_t )DIV_CEIL( ( int32_t )( 4 * tSymbolInUs ) -
                                                 ( int32_t )DIV_CEIL( ( *windowTimeoutInSymbols * tSymbolInUs ), 2 ) -
                                                 ( int32_t )( wakeUpTimeInMs * 1000 ), 1000 );
  }
  
  int8_t RegionCommonComputeTxPower( int8_t txPowerIndex, float maxEirp, float antennaGain )
  {
      int8_t phyTxPower = 0;
  
      phyTxPower = ( int8_t )floor( ( maxEirp - ( txPowerIndex * 2U ) ) - antennaGain );
  
      return phyTxPower;
  }
  
  void RegionCommonRxBeaconSetup( RegionCommonRxBeaconSetupParams_t* rxBeaconSetupParams )
  {
      bool rxContinuous = true;
      uint8_t datarate;
  
      // Set the radio into sleep mode
      Radio.Sleep( );
  
      // Setup frequency and payload length
      Radio.SetChannel( rxBeaconSetupParams->Frequency );
      Radio.SetMaxPayloadLength( MODEM_LORA, rxBeaconSetupParams->BeaconSize );
  
      // Check the RX continuous mode
      if( rxBeaconSetupParams->RxTime != 0 )
      {
          rxContinuous = false;
      }
  
      // Get region specific datarate
      datarate = rxBeaconSetupParams->Datarates[rxBeaconSetupParams->BeaconDatarate];
  
      // Setup radio
      Radio.SetRxConfig( MODEM_LORA, rxBeaconSetupParams->BeaconChannelBW, datarate,
                         1, 0, 10, rxBeaconSetupParams->SymbolTimeout, true, rxBeaconSetupParams->BeaconSize, false, 0, 0, false, rxContinuous );
  
      Radio.Rx( rxBeaconSetupParams->RxTime );
      /* ST_WORKAROUND_BEGIN: Print Beacon parameters */
      MW_LOG(TS_ON, VLEVEL_M, "RX_BC on freq %d Hz at DR %d\r\n", rxBeaconSetupParams->Frequency, rxBeaconSetupParams->BeaconDatarate );
      /* ST_WORKAROUND_END */
  }
  
  void RegionCommonCountNbOfEnabledChannels( RegionCommonCountNbOfEnabledChannelsParams_t* countNbOfEnabledChannelsParams,
                                             uint8_t* enabledChannels, uint8_t* nbEnabledChannels, uint8_t* nbRestrictedChannels )
  {
      uint8_t nbChannelCount = 0;
      uint8_t nbRestrictedChannelsCount = 0;
  
      for( uint8_t i = 0, k = 0; i < countNbOfEnabledChannelsParams->MaxNbChannels; i += 16, k++ )
      {
          for( uint8_t j = 0; j < 16; j++ )
          {
              if( ( countNbOfEnabledChannelsParams->ChannelsMask[k] & ( 1 << j ) ) != 0 )
              {
                  if( countNbOfEnabledChannelsParams->Channels[i + j].Frequency == 0 )
                  { // Check if the channel is enabled
                      continue;
                  }
                  if( ( countNbOfEnabledChannelsParams->Joined == false ) &&
                      ( countNbOfEnabledChannelsParams->JoinChannels != NULL ) )
                  {
                      if( ( countNbOfEnabledChannelsParams->JoinChannels[k] & ( 1 << j ) ) == 0 )
                      {
                          continue;
                      }
                  }
                  if( RegionCommonValueInRange( countNbOfEnabledChannelsParams->Datarate,
                                                countNbOfEnabledChannelsParams->Channels[i + j].DrRange.Fields.Min,
                                                countNbOfEnabledChannelsParams->Channels[i + j].DrRange.Fields.Max ) == false )
                  { // Check if the current channel selection supports the given datarate
                      continue;
                  }
                  if( countNbOfEnabledChannelsParams->Bands[countNbOfEnabledChannelsParams->Channels[i + j].Band].ReadyForTransmission == false )
                  { // Check if the band is available for transmission
                      nbRestrictedChannelsCount++;
                      continue;
                  }
                  enabledChannels[nbChannelCount++] = i + j;
              }
          }
      }
      *nbEnabledChannels = nbChannelCount;
      *nbRestrictedChannels = nbRestrictedChannelsCount;
  }
  
  LoRaMacStatus_t RegionCommonIdentifyChannels( RegionCommonIdentifyChannelsParam_t* identifyChannelsParam,
                                                TimerTime_t* aggregatedTimeOff, uint8_t* enabledChannels,
                                                uint8_t* nbEnabledChannels, uint8_t* nbRestrictedChannels,
                                                TimerTime_t* nextTxDelay )
  {
      TimerTime_t elapsed = TimerGetElapsedTime( identifyChannelsParam->LastAggrTx );
      *nextTxDelay = identifyChannelsParam->AggrTimeOff - elapsed;
      *nbRestrictedChannels = 1;
      *nbEnabledChannels = 0;
  
      if( ( identifyChannelsParam->LastAggrTx == 0 ) ||
          ( identifyChannelsParam->AggrTimeOff <= elapsed ) )
      {
          // Reset Aggregated time off
          *aggregatedTimeOff = 0;
  
          // Update bands Time OFF
          *nextTxDelay = RegionCommonUpdateBandTimeOff( identifyChannelsParam->CountNbOfEnabledChannelsParam->Joined,
                                                        identifyChannelsParam->CountNbOfEnabledChannelsParam->Bands,
                                                        identifyChannelsParam->MaxBands,
                                                        identifyChannelsParam->DutyCycleEnabled,
                                                        identifyChannelsParam->LastTxIsJoinRequest,
                                                        identifyChannelsParam->ElapsedTimeSinceStartUp,
                                                        identifyChannelsParam->ExpectedTimeOnAir );
  
          RegionCommonCountNbOfEnabledChannels( identifyChannelsParam->CountNbOfEnabledChannelsParam, enabledChannels,
                                                nbEnabledChannels, nbRestrictedChannels );
      }
  
      if( *nbEnabledChannels > 0 )
      {
          *nextTxDelay = 0;
          return LORAMAC_STATUS_OK;
      }
      else if( *nbRestrictedChannels > 0 )
      {
          return LORAMAC_STATUS_DUTYCYCLE_RESTRICTED;
      }
      else
      {
          return LORAMAC_STATUS_NO_CHANNEL_FOUND;
      }
  }
  
  int8_t RegionCommonGetNextLowerTxDr( RegionCommonGetNextLowerTxDrParams_t *params )
  {
      int8_t drLocal = params->CurrentDr;
  
      if( params->CurrentDr == params->MinDr )
      {
          return params->MinDr;
      }
      else
      {
          do
          {
              drLocal = ( drLocal - 1 );
          } while( ( drLocal != params->MinDr ) &&
                   ( RegionCommonChanVerifyDr( params->NbChannels, params->ChannelsMask, drLocal, params->MinDr, params->MaxDr, params->Channels  ) == false ) );
  
          return drLocal;
      }
  }
  
  int8_t RegionCommonLimitTxPower( int8_t txPower, int8_t maxBandTxPower )
  {
      // Limit tx power to the band max
      return MAX( txPower, maxBandTxPower );
  }
  
  uint32_t RegionCommonGetBandwidth( uint32_t drIndex, const uint32_t* bandwidths )
  {
      switch( bandwidths[drIndex] )
      {
          default:
          case 125000:
              return 0;
          case 250000:
              return 1;
          case 500000:
              return 2;
      }
  }
  
  /* ST_WORKAROUND_BEGIN: Print Tx/Rx config */
  void RegionCommonRxConfigPrint(LoRaMacRxSlot_t rxSlot, uint32_t frequency, int8_t dr)
  {
      const char *slotStrings[] = { "1", "2", "C", "Multi_C", "P", "Multi_P" };
  
      if ( rxSlot < RX_SLOT_NONE )
      {
          MW_LOG(TS_ON, VLEVEL_M,  "RX_%s on freq %d Hz at DR %d\r\n", slotStrings[rxSlot], frequency, dr );
      }
      else
      {
          MW_LOG(TS_ON, VLEVEL_M,  "RX on freq %d Hz at DR %d\r\n", frequency, dr );
      }
  }
  
  void RegionCommonTxConfigPrint(uint32_t frequency, int8_t dr)
  {
      MW_LOG(TS_ON, VLEVEL_M,  "TX on freq %d Hz at DR %d\r\n", frequency, dr );
  }
  /* ST_WORKAROUND_END */
  