/*! * \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 ) */ #include #include "radio.h" #include "utilities.h" #include "RegionCommon.h" #include "systime.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( datarate == 0x0F ) { // 0xF means that the device MUST ignore that field, and keep the current parameter value. datarate = verifyParams->CurrentDatarate; } else if( RegionCommonChanVerifyDr( verifyParams->NbChannels, verifyParams->ChannelsMask, datarate, verifyParams->MinDatarate, verifyParams->MaxDatarate, verifyParams->Channels ) == false ) { status &= 0xFD; // Datarate KO } // Verify tx power if( txPower == 0x0F ) { // 0xF means that the device MUST ignore that field, and keep the current parameter value. txPower = verifyParams->CurrentTxPower; } else 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 ) { // Set nbRep to the default value of 1. 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 ); } 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; } }