diff --git a/include/openthread/link.h b/include/openthread/link.h index 16f343ac8..66fda248e 100644 --- a/include/openthread/link.h +++ b/include/openthread/link.h @@ -597,6 +597,18 @@ bool otLinkIsPromiscuous(otInstance *aInstance); */ otError otLinkSetPromiscuous(otInstance *aInstance, bool aPromiscuous); + +/** + * This function returns the current CCA (Clear Channel Assessment) failure rate. + * + * The rate is maintained over a window of (roughly) last `OPENTHREAD_CONFIG_CCA_FAILURE_RATE_AVERAGING_WINDOW` + * frame transmissions. + * + * @returns The CCA failure rate with maximum value `0xffff` corresponding to 100% failure rate. + * + */ +uint16_t otLinkGetCcaFailureRate(otInstance *aInstance); + /** * @} * diff --git a/src/core/api/link_api.cpp b/src/core/api/link_api.cpp index 0538bd7f8..21ada295c 100644 --- a/src/core/api/link_api.cpp +++ b/src/core/api/link_api.cpp @@ -365,3 +365,10 @@ otError otLinkOutOfBandTransmitRequest(otInstance *aInstance, otRadioFrame *aOob return instance.GetThreadNetif().GetMac().SendOutOfBandFrameRequest(aOobFrame); } + +uint16_t otLinkGetCcaFailureRate(otInstance *aInstance) +{ + Instance &instance = *static_cast(aInstance); + + return instance.GetThreadNetif().GetMac().GetCcaFailureRate(); +} diff --git a/src/core/mac/mac.cpp b/src/core/mac/mac.cpp index 7c0be95e8..e9b069db2 100644 --- a/src/core/mac/mac.cpp +++ b/src/core/mac/mac.cpp @@ -118,7 +118,9 @@ Mac::Mac(Instance &aInstance): #endif // OPENTHREAD_ENABLE_MAC_FILTER mTxFrame(static_cast(otPlatRadioGetTransmitBuffer(&aInstance))), mOobFrame(NULL), - mKeyIdMode2FrameCounter(0) + mKeyIdMode2FrameCounter(0), + mCcaSuccessRateTracker(), + mCcaSampleCount(0) { GenerateExtAddress(&mExtAddress); @@ -1066,21 +1068,35 @@ void Mac::HandleTransmitDone(otRadioFrame *aFrame, otRadioFrame *aAckFrame, otEr { Frame &sendFrame = *static_cast(aFrame); bool framePending = false; + bool ccaSuccess = true; Address dstAddr; // Stop the ack timer. + mMacTimer.Stop(); + // Record CCA success or failure status. + switch (aError) { case OT_ERROR_ABORT: mCounters.mTxErrAbort++; + break; + + case OT_ERROR_CHANNEL_ACCESS_FAILURE: + ccaSuccess = false; // fall through case OT_ERROR_NONE: case OT_ERROR_NO_ACK: - case OT_ERROR_CHANNEL_ACCESS_FAILURE: + + if (mCcaSampleCount < kMaxCcaSampleCount) + { + mCcaSampleCount++; + } + + mCcaSuccessRateTracker.AddSample(ccaSuccess, mCcaSampleCount); break; default: diff --git a/src/core/mac/mac.hpp b/src/core/mac/mac.hpp index 6bea3afd3..b502ef372 100644 --- a/src/core/mac/mac.hpp +++ b/src/core/mac/mac.hpp @@ -44,6 +44,7 @@ #include "mac/mac_frame.hpp" #include "mac/mac_filter.hpp" #include "thread/key_manager.hpp" +#include "thread/link_quality.hpp" #include "thread/network_diagnostic_tlvs.hpp" #include "thread/topology.hpp" @@ -623,10 +624,22 @@ public: */ bool RadioSupportsRetries(void); + /** + * This method returns the current CCA (Clear Channel Assessment) failure rate. + * + * The rate is maintained over a window of (roughly) last `OPENTHREAD_CONFIG_CCA_FAILURE_RATE_AVERAGING_WINDOW` + * frame transmissions. + * + * @returns The CCA failure rate with maximum value `0xffff` corresponding to 100% failure rate. + * + */ + uint16_t GetCcaFailureRate(void) const { return mCcaSuccessRateTracker.GetFailureRate(); } + private: enum { - kInvalidRssiValue = 127 + kInvalidRssiValue = 127, + kMaxCcaSampleCount = OPENTHREAD_CONFIG_CCA_FAILURE_RATE_AVERAGING_WINDOW, }; enum Operation @@ -739,6 +752,9 @@ private: otMacCounters mCounters; uint32_t mKeyIdMode2FrameCounter; + + SuccessRateTracker mCcaSuccessRateTracker; + uint16_t mCcaSampleCount; }; /** diff --git a/src/core/openthread-core-default-config.h b/src/core/openthread-core-default-config.h index 08e9ab187..f564f0c0b 100644 --- a/src/core/openthread-core-default-config.h +++ b/src/core/openthread-core-default-config.h @@ -923,6 +923,19 @@ #define OPENTHREAD_CONFIG_ENABLE_STEERING_DATA_SET_OOB 0 #endif +/** + * @def OPENTHREAD_CONFIG_CCA_FAILURE_RATE_AVERAGING_WINDOW + * + * OpenThread's MAC implementation maintains the average failure rate of CCA (Clear Channel Assessment) operation on + * frame transmissions. This value specifies the window (in terms of number of transmissions or samples) over which the + * average rate is maintained. Practically, the average value can be considered as the percentage of CCA failures in + * (approximately) last AVERAGING_WINDOW frame transmissions. + * + */ +#ifndef OPENTHREAD_CONFIG_CCA_FAILURE_RATE_AVERAGING_WINDOW +#define OPENTHREAD_CONFIG_CCA_FAILURE_RATE_AVERAGING_WINDOW 512 +#endif + /** * @def OPENTHREAD_CONFIG_CHANNEL_MONITOR_SAMPLE_INTERVAL * diff --git a/src/core/thread/link_quality.cpp b/src/core/thread/link_quality.cpp index 90928d655..3a86db202 100644 --- a/src/core/thread/link_quality.cpp +++ b/src/core/thread/link_quality.cpp @@ -48,6 +48,18 @@ static const char *const kDigitsString[8] = "0", "125", "25", "375", "5", "625", "75", "875" }; +void SuccessRateTracker::AddSample(bool aSuccess, uint16_t aWeight) +{ + uint32_t oldAverage = mSuccessRate; + uint32_t newValue = (aSuccess) ? kMaxRateValue : 0; + uint32_t n = aWeight; + + // `n/2` is added to the sum to ensure rounding the value to the nearest integer when dividing by `n` + // (e.g., 1.2 -> 1, 3.5 -> 4). + + mSuccessRate = static_cast(((oldAverage * (n - 1)) + newValue + (n / 2)) / n); +} + void RssAverager::Reset(void) { mAverage = 0; diff --git a/src/core/thread/link_quality.hpp b/src/core/thread/link_quality.hpp index 9bfcc02bc..e758e3988 100644 --- a/src/core/thread/link_quality.hpp +++ b/src/core/thread/link_quality.hpp @@ -50,6 +50,68 @@ namespace ot { * @{ */ +/** + * This class implements an operation Success Rate Tracker. + * + * This can be used to track different link quality related metrics, e.g., CCA failure rate, frame tx success rate). + * The success rate is maintained using an exponential moving IIR averaging filter with a `uint16_t` as the storage. + * + */ +class SuccessRateTracker +{ +public: + enum + { + kMaxRateValue = 0xffff, ///< Indicates value corresponding to maximum (failure/success) rate of 100%. + }; + + /** + * This constructor initializes a `SuccessRateTracker` instance. + * + * After initialization the tracker starts with success rate 100% (failure rate 0%). + * + */ + SuccessRateTracker(void): mSuccessRate(kMaxRateValue) { } + + /** + * This method resets the tracker to its initialized state, setting success rate to 100%. + * + */ + void Reset(void) { mSuccessRate = kMaxRateValue; } + + /** + * This method adds a sample (success or failure) to `SuccessRateTracker`. + * + * @param[in] aSuccess The sample status be added, `true` for success, `false` for failure. + * @param[in] aWeight The weight coefficient used for adding the new sample into average. + * + */ + void AddSample(bool aSuccess, uint16_t aWeight = kDefaultWeight); + + /** + * This method returns the average failure rate. + * + * @retval the average failure rate `[0-kMaxRateValue]` with `kMaxRateValue` corresponding to 100%. + * + */ + uint16_t GetFailureRate(void) const { return kMaxRateValue - mSuccessRate; } + + /** + * This method returns the average success rate. + * + * @retval the average success rate as [0-kMaxRateValue] with `kMaxRateValue` corresponding to 100%. + * + */ + uint16_t GetSuccessRate(void) const { return mSuccessRate; } + +private: + enum + { + kDefaultWeight = 64, + }; + + uint16_t mSuccessRate; +}; /** * This class implements a Received Signal Strength (RSS) averager. @@ -191,7 +253,7 @@ public: void AddRss(int8_t aNoiseFloor, int8_t aRss); /** - * This method returns the current average signal strength value. + * This method returns the current average received signal strength value. * * @returns The current average value or @c OT_RADIO_RSSI_INVALID if no average is available. * @@ -208,7 +270,7 @@ public: uint16_t GetAverageRssRaw(void) const { return mRssAverager.GetRaw(); } /** - * This method converts the link quality info to NULL-terminated info/debug human-readable string. + * This method converts the link quality info to NULL-terminated info/debug human-readable string. * * @param[out] aBuf A pointer to the string buffer. * @param[in] aSize The maximum size of the string buffer. diff --git a/tests/unit/test_link_quality.cpp b/tests/unit/test_link_quality.cpp index 405a0c577..d82895f98 100644 --- a/tests/unit/test_link_quality.cpp +++ b/tests/unit/test_link_quality.cpp @@ -287,7 +287,7 @@ void TestRssAveraging(void) } //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Adding two alteraing values many times: + // Adding two alternating values many times: printf("- - - - - - - - - - - - - - - - - -\n"); @@ -394,6 +394,114 @@ void TestLinkQualityCalculations(void) TestLinkQualityData(rssData4); } +void TestSuccessRateTracker(void) +{ + SuccessRateTracker rateTracker; + uint16_t sampleCount; + + const uint16_t kMaxSamples = 5000; + + const uint16_t kMaxRate = SuccessRateTracker::kMaxRateValue; + const double kMaxError = 1.0; // Max permitted error in percentage + const uint16_t kWeightLimit[] = { 64, 128, 256, 300, 512, 810, 900 }; + + printf("\nTesting SuccessRateTracker\n"); + + VerifyOrQuit(rateTracker.GetSuccessRate() == kMaxRate, "SuccessRateTracker: Initial value incorrect"); + VerifyOrQuit(rateTracker.GetFailureRate() == 0, "SuccessRateTracker: Initial value incorrect"); + + // Adding all success + for (sampleCount = 1; sampleCount < kMaxSamples; sampleCount++) + { + rateTracker.AddSample(true, sampleCount); + + VerifyOrQuit(rateTracker.GetSuccessRate() == kMaxRate, "SuccessRateTracker: incorrect rate all success case"); + VerifyOrQuit(rateTracker.GetFailureRate() == 0, "SuccessRateTracker: incorrect rate in all success case"); + } + + rateTracker.Reset(); + VerifyOrQuit(rateTracker.GetSuccessRate() == kMaxRate, "SuccessRateTracker: Rate incorrect after reset"); + VerifyOrQuit(rateTracker.GetFailureRate() == 0, "SuccessRateTracker: Rate incorrect after reset"); + + // Adding all failures + for (sampleCount = 1; sampleCount < kMaxRate; sampleCount++) + { + rateTracker.AddSample(false, sampleCount); + + VerifyOrQuit(rateTracker.GetSuccessRate() == 0, "SuccessRateTracker: rate incorrect all failure case"); + VerifyOrQuit(rateTracker.GetFailureRate() == kMaxRate, "SuccessRateTracker: rate incorrect in all failure case"); + } + + // Adding success/failure at different rates and checking the RateTracker rate for every sample + + for (uint16_t testRound = 0; testRound < sizeof(kWeightLimit) / sizeof(kWeightLimit[0]) * 2; testRound++) + { + uint16_t weightLimit; + bool reverseLogic; + double maxDiff = 0; + + // Reverse the logic (add success instead of failure) on even test rounds + reverseLogic = ((testRound % 2) == 0); + + // Select a different weight limit based on the current test round + weightLimit = kWeightLimit[testRound / 2]; + + printf("TestRound %02d, weightLimit %3d, reverseLogic %d ", testRound, weightLimit, reverseLogic); + + for (uint16_t period = 1; period < 101; period++) + { + uint16_t failureCount = 0; + + rateTracker.Reset(); + + for (sampleCount = 1; sampleCount < kMaxSamples; sampleCount++) + { + double expectedRate; + double failureRate; + double diff; + bool isSuccess = ((sampleCount % period) == 0); + uint16_t weight; + + if (reverseLogic) + { + isSuccess = !isSuccess; + } + + weight = sampleCount; + + if (weight > weightLimit) + { + weight = weightLimit; + } + + rateTracker.AddSample(isSuccess, weight); + + if (!isSuccess) + { + failureCount++; + } + + // Calculate the failure rate from rateTracker and expected rate. + + failureRate = static_cast(rateTracker.GetFailureRate()) * 100.0 / kMaxRate; // in percent + expectedRate = static_cast(failureCount) * 100.0 / sampleCount; // in percent + + diff = failureRate - expectedRate; + diff = ABS(diff); + + VerifyOrQuit(diff <= kMaxError, "SuccessRateTracker: rate does not match expected value"); + + if (diff > maxDiff) + { + maxDiff = diff; + } + } + } + + printf(" MaxDiff = %.3f%%-> PASS\n", maxDiff); + } +} + } // namespace ot #ifdef ENABLE_TEST_MAIN @@ -401,7 +509,8 @@ int main(void) { ot::TestRssAveraging(); ot::TestLinkQualityCalculations(); - printf("All tests passed\n"); + ot::TestSuccessRateTracker(); + printf("\nAll tests passed\n"); return 0; } #endif